From 0ee66ef66326db2c035b1b10ecd56bb8ad72687c Mon Sep 17 00:00:00 2001 From: Ari24-cb24 Date: Sun, 1 May 2022 14:24:34 +0200 Subject: [PATCH 01/57] Fixed bugs and added half an hour relatives (WIP) --- datetimeparser/parsermethods.py | 66 +++++++++++++++++---------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/datetimeparser/parsermethods.py b/datetimeparser/parsermethods.py index d2ef220..0dfc1c7 100644 --- a/datetimeparser/parsermethods.py +++ b/datetimeparser/parsermethods.py @@ -113,15 +113,15 @@ def parse(self, string: str) -> Union[None, Tuple[MethodEnum, RelativeDateTime]] # Checks if the string starts with a preposition # And then cuts it off because we need the preposition to differentiate future and past events for preposition in self.PREPOSITIONS: - if string.startswith(preposition): - string = string[len(preposition):] + if string.split()[0] == preposition: + string = " ".join(string.split()[1:]) break else: preposition = "" new_data = [] - for argument in string.split(): + for idx, argument in enumerate(string.split()): not_possible = True argument = argument.lower().strip(",") @@ -130,7 +130,13 @@ def parse(self, string: str) -> Union[None, Tuple[MethodEnum, RelativeDateTime]] continue # an `a` always represents a 1 (e.g. a day = 1 day) - if argument.lower() == "a": + # But there is one special case: `quarter an hour`. This means 0.25 hours or 15 minutes + if argument.lower() in ("a", "an"): + if new_data and new_data[-1] == DatetimeConstants.QUARTERS: + if idx + 1 < len(string.split()) and string.split()[idx + 1] in DatetimeConstants.HOURS.get_all(): + # We break because there can't be anything after "quarter an hour" + break + new_data.append(1 if preposition != "last" else -1) not_possible = False @@ -248,8 +254,8 @@ def parse(self, string: str) -> Optional[Tuple[MethodEnum, Tuple]]: # noqa: C90 # Cut off 'at', 'at the' and 'the' for cutoff_word in self.CUTOFF_KEYWORDS: - if string.startswith(cutoff_word): - string = string[len(cutoff_word):] + if string.split()[0] == cutoff_word: + string = " ".join(string.split()[1:]) # Strip whitespace string = " ".join(string.split()) @@ -257,8 +263,8 @@ def parse(self, string: str) -> Optional[Tuple[MethodEnum, Tuple]]: # noqa: C90 # Cut off the preposition if the strings starts with one and save the preposition # To differentiate future and past for preposition in self.PREPOSITIONS: - if string.startswith(preposition): - string = string[len(preposition):] + if string.split()[0] == preposition: + string = " ".join(string.split()[1:]) break else: preposition = None @@ -325,7 +331,7 @@ def _find_number(string: str) -> Optional[int]: :return: The number if found, None otherwise """ - if string == "a": + if string in ("a", "an"): return 1 if parse_int(string): @@ -399,8 +405,8 @@ def _get_preposition(cls, string: str) -> Optional[Tuple[str, str]]: # Cut off the preposition if the strings starts with one and save the preposition # To differentiate future and past for preposition in cls.PREPOSITIONS: - if string.startswith(preposition): - string = string[len(preposition):] + if string.split()[0] == preposition: + string = " ".join(string.split()[1:]) break else: preposition = "at" @@ -418,9 +424,8 @@ def _get_preposition_keyword(cls, string: str) -> Optional[Tuple[str, str, Union """ # Cut off 'at', 'at the' and 'the' for cutoff_word in cls.CUTOFF_KEYWORDS: - if string.startswith(cutoff_word): - string = string[len(cutoff_word):] - string = string.strip() + if string.split()[0] == cutoff_word: + string = " ".join(string.split()[1:]) preposition, string = cls._get_preposition(string) @@ -688,7 +693,7 @@ def _split_data(self, string: str) -> Optional[Tuple[dict, dict, dict]]: """ # If none of the known prepositions are in the string, return None - if not any(absolute_preposition.name in string for absolute_preposition in self.ABSOLUTE_PREPOSITION_TOKENS): + if not any(absolute_preposition.name in string.split() for absolute_preposition in self.ABSOLUTE_PREPOSITION_TOKENS): return None word = None @@ -706,7 +711,7 @@ def _split_data(self, string: str) -> Optional[Tuple[dict, dict, dict]]: relative = string[:char_count - len(word) - 2] absolute = string[char_count:] - # There may be more prepositions in the absolute part so we try it again via recursion + # There may be more prepositions in the absolute part, so we try it again via recursion recursion_result = self._split_data(absolute) if recursion_result is not None: @@ -734,7 +739,7 @@ def _parse_relative_statement(self, relative_statement: str, preposition: str) - continue # 'a' means the same as '1' (e.g. 'a day' => '1 day') - if argument == "a": + if argument in ("a", "an"): returned_data.append(1) continue @@ -745,19 +750,18 @@ def _parse_relative_statement(self, relative_statement: str, preposition: str) - # '1st', '1.', 'first', ... # 'one', 'two', 'three', ... # 'seconds', 'minutes', 'hours', ... - for keyword in self.RELATIVE_DATETIME_CONSTANTS if preposition != "past" else (*self.RELATIVE_DATETIME_CONSTANTS, self.RELATIVE_TIME_CONSTANTS): - for alias in keyword.get_all(): - if alias == argument: - if keyword in DatetimeConstants.ALL: - if not returned_data or not isinstance(returned_data[-1], int): - # For cases like 'day and month (before christmas)' - returned_data.append(1) - - returned_data.append(keyword) - else: - returned_data.append(keyword.value) + 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): + # For cases like 'day and month (before christmas)' + returned_data.append(1) - break + returned_data.append(keyword) + else: + returned_data.append(keyword.value) + + break else: continue @@ -825,11 +829,11 @@ def _parse_absolute_statement(self, data: Union[str, Tuple]) -> Optional: if isinstance(data, str): # Try constants (e.g. "(three days after) christmas") constants_parser = ConstantsParser() - constants_parser.CONSTANT_KEYWORDS = (*Constants.ALL, *DatetimeDeltaConstants.ALL, *MonthConstants.ALL, *WeekdayConstants.ALL) + constants_parser.CONSTANT_KEYWORDS = (*Constants.ALL, *DatetimeDeltaConstants.ALL, *DatetimeConstants.ALL, *MonthConstants.ALL, *WeekdayConstants.ALL) constants_parser.PREPOSITIONS = ("last", "next", "this", "previous") constants_parser.PAST_PREPOSITIONS = ("last", "previous") constants_parser.FUTURE_PREPOSITIONS = ("next", "this") - constants_parser.CUTOFF_KEYWORDS = ("at the", "the", "at") + constants_parser.CUTOFF_KEYWORDS = ("at the", "the", "at", "an", "a") result = constants_parser.parse(data) From 2d5d2f384d08e15c836edef474df52981c3573c2 Mon Sep 17 00:00:00 2001 From: Ari24-cb24 Date: Sun, 1 May 2022 19:15:47 +0200 Subject: [PATCH 02/57] Fixed typing issue --- datetimeparser/evaluatorutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datetimeparser/evaluatorutils.py b/datetimeparser/evaluatorutils.py index 55cf7f1..5326d0d 100644 --- a/datetimeparser/evaluatorutils.py +++ b/datetimeparser/evaluatorutils.py @@ -58,7 +58,7 @@ def datetime_to_absolute_datetime(dt: datetime) -> AbsoluteDateTime: return absdt @staticmethod - def sanitize_input(current_time: datetime, parsed_list: list) -> list[Union[RelativeDateTime, AbsoluteDateTime, int, Constant]]: + def sanitize_input(current_time: datetime, parsed_list: list) -> List[Union[RelativeDateTime, AbsoluteDateTime, int, Constant]]: """ Removes useless keywords :param parsed_list: The list that should be sanitized From 368f578ddb49d99fb2759a863e83659ef3cb9468 Mon Sep 17 00:00:00 2001 From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com> Date: Sun, 5 Jun 2022 14:42:46 +0200 Subject: [PATCH 03/57] Added custom exception + fixed runtest.py --- datetimeparser/evaluator.py | 5 +++-- datetimeparser/evaluatormethods.py | 5 +++-- datetimeparser/exceptions.py | 23 +++++++++++++++++++++++ tests/runtests.py | 4 ++-- 4 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 datetimeparser/exceptions.py diff --git a/datetimeparser/evaluator.py b/datetimeparser/evaluator.py index d6efd8e..d313a24 100644 --- a/datetimeparser/evaluator.py +++ b/datetimeparser/evaluator.py @@ -5,6 +5,7 @@ from .baseclasses import AbsoluteDateTime, RelativeDateTime from .enums import Method from .evaluatormethods import EvaluatorMethods +from .exceptions import FailedEvaluation, InvalidValue class Evaluator: @@ -17,7 +18,7 @@ def __init__(self, parsed_object, tz="Europe/Berlin"): try: tiz = timezone(tz) except UnknownTimeZoneError: - raise ValueError("Unknown timezone: {}".format(tz)) + raise InvalidValue(f"Unknown timezone: '{tz}'") self.parsed_object_type = parsed_object[0] self.parsed_object_content: Union[list, AbsoluteDateTime, RelativeDateTime] = parsed_object[1] @@ -49,4 +50,4 @@ def evaluate(self) -> Union[datetime, None]: if ev_out: return ev_out else: - raise ValueError + raise FailedEvaluation(self.parsed_object_content) diff --git a/datetimeparser/evaluatormethods.py b/datetimeparser/evaluatormethods.py index 151df37..baeaf4a 100644 --- a/datetimeparser/evaluatormethods.py +++ b/datetimeparser/evaluatormethods.py @@ -1,6 +1,7 @@ from .baseclasses import * from .enums import * from .evaluatorutils import EvaluatorUtils +from .exceptions import InvalidValue class EvaluatorMethods(EvaluatorUtils): @@ -109,8 +110,8 @@ def evaluate_constants(self) -> datetime: dt = object_type.time_value(object_year + 1) else: - if object_type.name == "infinity": - raise ValueError("'infinity' isn't a valid time") + if object_type.name == "infinity": # TODO: has to be improved for more invalid constants if needed + raise InvalidValue(object_type.name) elif object_type in WeekdayConstants.ALL: dt: datetime = datetime.strptime( diff --git a/datetimeparser/exceptions.py b/datetimeparser/exceptions.py new file mode 100644 index 0000000..ded9de0 --- /dev/null +++ b/datetimeparser/exceptions.py @@ -0,0 +1,23 @@ +class EvaluatorException(Exception): + """ + Base class for Evaluator Exceptions + """ + def __init__(self, error_type: str, errors: str): + self.errors = errors + self.error_type = error_type + super().__init__(error_type) + + def __str__(self): + return f"{self.error_type}: {self.errors}" + + +class FailedEvaluation(EvaluatorException): + def __init__(self, tried: str | list = None): + + super().__init__(type(self).__name__, f"Cannot evaluate {tried} into datetime") + + +class InvalidValue(EvaluatorException): + def __init__(self, value: str): + + super().__init__(type(self).__name__, f"{value}") diff --git a/tests/runtests.py b/tests/runtests.py index 3a799ca..4c9bc8b 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -116,7 +116,7 @@ def evaluate_testcases(testcase_results: dict, disable_color=False, disable_inde elif StatusType.NO_VALIDATION < status < StatusType.WRONG_RESULT: if status in (StatusType.PARSER_RETURNS_NONE, StatusType.EVALUATOR_RETURNS_NONE): message = f"{Colors.ANSI_CYAN}{'Parser' if status == StatusType.PARSER_RETURNS_NONE else 'Evaluator'} {Colors.ANSI_BOLD_WHITE}returned {Colors.ANSI_LIGHT_RED}None" - elif status in (StatusType.PARSER_EXCEPTION, StatusType.EVALUATOR_RETURNS_NONE): + elif status in (StatusType.PARSER_EXCEPTION, StatusType.EVALUATOR_EXCEPTION): message = f"{Colors.ANSI_CYAN}{'Parser' if status == StatusType.PARSER_EXCEPTION else 'Evaluator'} {Colors.ANSI_BOLD_WHITE}raised an {Colors.ANSI_LIGHT_RED}exception: {Colors.ANSI_WHITE}{info}" else: continue @@ -132,7 +132,7 @@ def evaluate_testcases(testcase_results: dict, disable_color=False, disable_inde elif StatusType.NO_VALIDATION < status < StatusType.WRONG_RESULT: if status in (StatusType.PARSER_RETURNS_NONE, StatusType.EVALUATOR_RETURNS_NONE): message = f"{'Parser' if status == StatusType.PARSER_RETURNS_NONE else 'Evaluator'} returned None" - elif status in (StatusType.PARSER_EXCEPTION, StatusType.EVALUATOR_RETURNS_NONE): + elif status in (StatusType.PARSER_EXCEPTION, StatusType.EVALUATOR_EXCEPTION): message = f"{'Parser' if status == StatusType.PARSER_EXCEPTION else 'Evaluator'} raised an exception: {info}" else: continue From 5f0c8b032a663d47229a44072fe0563da270410a Mon Sep 17 00:00:00 2001 From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com> Date: Sun, 5 Jun 2022 14:46:53 +0200 Subject: [PATCH 04/57] Fixed version --- datetimeparser/datetimeparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index d8e979b..815e0bb 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -3,7 +3,7 @@ """ __all__ = ['parse', '__version__', '__author__'] -__version__ = "0.12.2" +__version__ = "0.13.0" __author__ = "aridevelopment" import datetime From ad24a4a3f0872c088cddc7c2e6b1dc4652172edf Mon Sep 17 00:00:00 2001 From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com> Date: Sun, 12 Jun 2022 11:02:21 +0200 Subject: [PATCH 05/57] Updated README.md --- README.md | 12 ++++++++++++ datetimeparser/evaluator/__init__.py | 0 datetimeparser/{ => evaluator}/evaluator.py | 0 datetimeparser/{ => evaluator}/evaluatormethods.py | 0 datetimeparser/{ => evaluator}/evaluatorutils.py | 0 datetimeparser/parser/__init__.py | 0 datetimeparser/{ => parser}/parser.py | 0 datetimeparser/{ => parser}/parsermethods.py | 0 datetimeparser/utils/__init__.py | 0 datetimeparser/{ => utils}/baseclasses.py | 0 datetimeparser/{ => utils}/enums.py | 0 datetimeparser/{ => utils}/exceptions.py | 0 datetimeparser/{ => utils}/formulars.py | 0 editreadme.py | 2 +- 14 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 datetimeparser/evaluator/__init__.py rename datetimeparser/{ => evaluator}/evaluator.py (100%) rename datetimeparser/{ => evaluator}/evaluatormethods.py (100%) rename datetimeparser/{ => evaluator}/evaluatorutils.py (100%) create mode 100644 datetimeparser/parser/__init__.py rename datetimeparser/{ => parser}/parser.py (100%) rename datetimeparser/{ => parser}/parsermethods.py (100%) create mode 100644 datetimeparser/utils/__init__.py rename datetimeparser/{ => utils}/baseclasses.py (100%) rename datetimeparser/{ => utils}/enums.py (100%) rename datetimeparser/{ => utils}/exceptions.py (100%) rename datetimeparser/{ => utils}/formulars.py (100%) diff --git a/README.md b/README.md index d8c2c9f..5ffed22 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,18 @@ We highly appreciate everyone who wants to help our project!
  • valentine day
  • +pi day +
      +
    • piday
    • +
    • pi-day
    • +
    +
    +tau day +
      +
    • tauday
    • +
    • tau-day
    • +
    +
    summer end
    • end of summer
    • diff --git a/datetimeparser/evaluator/__init__.py b/datetimeparser/evaluator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/datetimeparser/evaluator.py b/datetimeparser/evaluator/evaluator.py similarity index 100% rename from datetimeparser/evaluator.py rename to datetimeparser/evaluator/evaluator.py diff --git a/datetimeparser/evaluatormethods.py b/datetimeparser/evaluator/evaluatormethods.py similarity index 100% rename from datetimeparser/evaluatormethods.py rename to datetimeparser/evaluator/evaluatormethods.py diff --git a/datetimeparser/evaluatorutils.py b/datetimeparser/evaluator/evaluatorutils.py similarity index 100% rename from datetimeparser/evaluatorutils.py rename to datetimeparser/evaluator/evaluatorutils.py diff --git a/datetimeparser/parser/__init__.py b/datetimeparser/parser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/datetimeparser/parser.py b/datetimeparser/parser/parser.py similarity index 100% rename from datetimeparser/parser.py rename to datetimeparser/parser/parser.py diff --git a/datetimeparser/parsermethods.py b/datetimeparser/parser/parsermethods.py similarity index 100% rename from datetimeparser/parsermethods.py rename to datetimeparser/parser/parsermethods.py diff --git a/datetimeparser/utils/__init__.py b/datetimeparser/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/datetimeparser/baseclasses.py b/datetimeparser/utils/baseclasses.py similarity index 100% rename from datetimeparser/baseclasses.py rename to datetimeparser/utils/baseclasses.py diff --git a/datetimeparser/enums.py b/datetimeparser/utils/enums.py similarity index 100% rename from datetimeparser/enums.py rename to datetimeparser/utils/enums.py diff --git a/datetimeparser/exceptions.py b/datetimeparser/utils/exceptions.py similarity index 100% rename from datetimeparser/exceptions.py rename to datetimeparser/utils/exceptions.py diff --git a/datetimeparser/formulars.py b/datetimeparser/utils/formulars.py similarity index 100% rename from datetimeparser/formulars.py rename to datetimeparser/utils/formulars.py diff --git a/editreadme.py b/editreadme.py index 4d5ee42..6f32ede 100644 --- a/editreadme.py +++ b/editreadme.py @@ -1,4 +1,4 @@ -from datetimeparser.enums import ( +from datetimeparser.utils.enums import ( Constants, MonthConstants, WeekdayConstants, From 0a137f5fabd86e6d77e12be85c99a76886ae29e6 Mon Sep 17 00:00:00 2001 From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com> Date: Sun, 12 Jun 2022 11:03:01 +0200 Subject: [PATCH 06/57] Updated file structure --- datetimeparser/__init__.py | 5 ++--- datetimeparser/evaluator/__init__.py | 1 + datetimeparser/evaluator/evaluator.py | 8 ++++---- datetimeparser/evaluator/evaluatormethods.py | 8 ++++---- datetimeparser/evaluator/evaluatorutils.py | 4 ++-- datetimeparser/parser/__init__.py | 1 + datetimeparser/parser/parser.py | 8 ++++---- datetimeparser/parser/parsermethods.py | 4 ++-- datetimeparser/utils/baseclasses.py | 2 +- datetimeparser/utils/enums.py | 4 ++-- 10 files changed, 23 insertions(+), 22 deletions(-) diff --git a/datetimeparser/__init__.py b/datetimeparser/__init__.py index 0193731..98d31a0 100644 --- a/datetimeparser/__init__.py +++ b/datetimeparser/__init__.py @@ -1,7 +1,6 @@ from datetimeparser import parser from datetimeparser import evaluator -from datetimeparser import enums -from datetimeparser import baseclasses -from datetimeparser import parsermethods +from datetimeparser.utils import baseclasses, enums +from datetimeparser.parser import parsermethods from datetimeparser.datetimeparser import parse diff --git a/datetimeparser/evaluator/__init__.py b/datetimeparser/evaluator/__init__.py index e69de29..6eedafc 100644 --- a/datetimeparser/evaluator/__init__.py +++ b/datetimeparser/evaluator/__init__.py @@ -0,0 +1 @@ +from .evaluator import Evaluator diff --git a/datetimeparser/evaluator/evaluator.py b/datetimeparser/evaluator/evaluator.py index d313a24..50980d9 100644 --- a/datetimeparser/evaluator/evaluator.py +++ b/datetimeparser/evaluator/evaluator.py @@ -2,10 +2,10 @@ from pytz import timezone, UnknownTimeZoneError from typing import Union -from .baseclasses import AbsoluteDateTime, RelativeDateTime -from .enums import Method -from .evaluatormethods import EvaluatorMethods -from .exceptions import FailedEvaluation, InvalidValue +from datetimeparser.utils.baseclasses import AbsoluteDateTime, RelativeDateTime +from datetimeparser.utils.enums import Method +from datetimeparser.evaluator.evaluatormethods import EvaluatorMethods +from datetimeparser.utils.exceptions import FailedEvaluation, InvalidValue class Evaluator: diff --git a/datetimeparser/evaluator/evaluatormethods.py b/datetimeparser/evaluator/evaluatormethods.py index baeaf4a..a72372e 100644 --- a/datetimeparser/evaluator/evaluatormethods.py +++ b/datetimeparser/evaluator/evaluatormethods.py @@ -1,7 +1,7 @@ -from .baseclasses import * -from .enums import * -from .evaluatorutils import EvaluatorUtils -from .exceptions import InvalidValue +from datetimeparser.evaluator.evaluatorutils import EvaluatorUtils +from datetimeparser.utils.baseclasses import * +from datetimeparser.utils.enums import * +from datetimeparser.utils.exceptions import InvalidValue class EvaluatorMethods(EvaluatorUtils): diff --git a/datetimeparser/evaluator/evaluatorutils.py b/datetimeparser/evaluator/evaluatorutils.py index 5326d0d..a262256 100644 --- a/datetimeparser/evaluator/evaluatorutils.py +++ b/datetimeparser/evaluator/evaluatorutils.py @@ -1,7 +1,7 @@ from typing import Union -from .baseclasses import * -from .enums import * +from datetimeparser.utils.baseclasses import * +from datetimeparser.utils.enums import * class EvaluatorUtils: diff --git a/datetimeparser/parser/__init__.py b/datetimeparser/parser/__init__.py index e69de29..2a3855a 100644 --- a/datetimeparser/parser/__init__.py +++ b/datetimeparser/parser/__init__.py @@ -0,0 +1 @@ +from .parser import Parser diff --git a/datetimeparser/parser/parser.py b/datetimeparser/parser/parser.py index a04edb7..59453d2 100644 --- a/datetimeparser/parser/parser.py +++ b/datetimeparser/parser/parser.py @@ -1,12 +1,12 @@ import string as string_utils -from .parsermethods import ( +from datetimeparser.parser.parsermethods import ( AbsoluteDateFormatsParser, - RelativeDatetimesParser, - ConstantsParser, + AbsolutePrepositionParser, ConstantRelativeExtensionsParser, + ConstantsParser, DatetimeDeltaConstantsParser, - AbsolutePrepositionParser + RelativeDatetimesParser ) diff --git a/datetimeparser/parser/parsermethods.py b/datetimeparser/parser/parsermethods.py index 0dfc1c7..24d2e81 100644 --- a/datetimeparser/parser/parsermethods.py +++ b/datetimeparser/parser/parsermethods.py @@ -1,8 +1,8 @@ import re from typing import Optional, Tuple, Union -from .enums import * -from .baseclasses import * +from datetimeparser.utils.enums import * +from datetimeparser.utils.baseclasses import * def parse_int(text: str) -> bool: diff --git a/datetimeparser/utils/baseclasses.py b/datetimeparser/utils/baseclasses.py index 70d8b9d..feaa9b4 100644 --- a/datetimeparser/utils/baseclasses.py +++ b/datetimeparser/utils/baseclasses.py @@ -3,7 +3,7 @@ from typing import Callable, List, TYPE_CHECKING if TYPE_CHECKING: - from datetimeparser.enums import ConstantOption # noqa: I2041 + from datetimeparser.utils.enums import ConstantOption # noqa: I2041 class Printable: diff --git a/datetimeparser/utils/enums.py b/datetimeparser/utils/enums.py index b2f4e0a..5404418 100644 --- a/datetimeparser/utils/enums.py +++ b/datetimeparser/utils/enums.py @@ -3,8 +3,8 @@ from dateutil.relativedelta import relativedelta -from .baseclasses import Constant, MethodEnum -from .formulars import days_feb, eastern_calc, thanksgiving_calc, year_start +from datetimeparser.utils.baseclasses import Constant, MethodEnum +from datetimeparser.utils.formulars import days_feb, eastern_calc, thanksgiving_calc, year_start class ConstantOption(Enum): From 299d7127943fd25413257aaab77e3f7b0623b37e Mon Sep 17 00:00:00 2001 From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com> Date: Sun, 12 Jun 2022 11:06:52 +0200 Subject: [PATCH 07/57] Updated version number --- datetimeparser/datetimeparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index 815e0bb..3f789ef 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -3,7 +3,7 @@ """ __all__ = ['parse', '__version__', '__author__'] -__version__ = "0.13.0" +__version__ = "0.14.0" __author__ = "aridevelopment" import datetime From 83065c027805f082282d8f11ed6f1307945016e5 Mon Sep 17 00:00:00 2001 From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com> Date: Sun, 12 Jun 2022 11:08:15 +0200 Subject: [PATCH 08/57] Updated version number --- datetimeparser/datetimeparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index 3f789ef..815e0bb 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -3,7 +3,7 @@ """ __all__ = ['parse', '__version__', '__author__'] -__version__ = "0.14.0" +__version__ = "0.13.0" __author__ = "aridevelopment" import datetime From dd3ee1495d1319ebc4145de0d0541a88774c182a Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 16 Jun 2022 20:21:09 +0200 Subject: [PATCH 09/57] Improved exceptions for evaluator + removed useless function --- datetimeparser/datetimeparser.py | 2 +- datetimeparser/evaluator/evaluatormethods.py | 10 +++++----- datetimeparser/evaluator/evaluatorutils.py | 19 ++++++++----------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index 815e0bb..65a6727 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -3,7 +3,7 @@ """ __all__ = ['parse', '__version__', '__author__'] -__version__ = "0.13.0" +__version__ = "0.13.1" __author__ = "aridevelopment" import datetime diff --git a/datetimeparser/evaluator/evaluatormethods.py b/datetimeparser/evaluator/evaluatormethods.py index a72372e..9e526ae 100644 --- a/datetimeparser/evaluator/evaluatormethods.py +++ b/datetimeparser/evaluator/evaluatormethods.py @@ -46,7 +46,7 @@ def evaluate_constant_relatives(self) -> datetime: ev_out = datetime(base.year, base.month, base.day, hour, minute, sec) elif isinstance(sanitized[-1], RelativeDateTime): - base += self.prepare_relative_delta(sanitized[-1]) + base = self.add_relative_delta(base, sanitized[-1]) if sanitized[-2] in WeekdayConstants.ALL: base = self.cut_time(base) @@ -93,9 +93,9 @@ def evaluate_absolute_prepositions(self) -> datetime: sanitized = self.sanitize_input(self.current_time, self.parsed) base = self.get_base(sanitized, base_year, self.current_time) rel_out = self.calc_relative_time(sanitized) - base += self.prepare_relative_delta(rel_out) + base = self.add_relative_delta(base, rel_out) - return self.remove_milli_seconds(base) + return base def evaluate_constants(self) -> datetime: dt: datetime = self.current_time @@ -140,14 +140,14 @@ def evaluate_constants(self) -> datetime: ) if object_type.offset: - ev_out += self.prepare_relative_delta(self.get_offset(object_type, self.offset)) + ev_out = self.add_relative_delta(ev_out, self.get_offset(object_type, self.offset)) return ev_out def evaluate_relative_datetime(self) -> datetime: out: datetime = self.current_time - out += self.prepare_relative_delta(self.parsed) + out = self.add_relative_delta(out, self.parsed) ev_out = datetime( out.year, out.month, out.day, out.hour, out.minute, out.second ) diff --git a/datetimeparser/evaluator/evaluatorutils.py b/datetimeparser/evaluator/evaluatorutils.py index a262256..e5d1ea1 100644 --- a/datetimeparser/evaluator/evaluatorutils.py +++ b/datetimeparser/evaluator/evaluatorutils.py @@ -2,6 +2,7 @@ from datetimeparser.utils.baseclasses import * from datetimeparser.utils.enums import * +from datetimeparser.utils.exceptions import InvalidValue class EvaluatorUtils: @@ -181,9 +182,10 @@ def calc_relative_time(sanitized_list: list) -> RelativeDateTime: return ev_out @staticmethod - def prepare_relative_delta(rel_time: RelativeDateTime) -> relativedelta: + def add_relative_delta(base_time: datetime, rel_time: RelativeDateTime) -> datetime: """ Prepares a RelativeDateTime-object for adding to a datetime + :param base_time: DateTime-object the time should be added too :param rel_time: RelativeDateTime-object :return: relativedelta """ @@ -198,17 +200,12 @@ def prepare_relative_delta(rel_time: RelativeDateTime) -> relativedelta: seconds=rel_time.seconds ) - return rel + try: + out = base_time + rel + except ValueError as e: + raise InvalidValue(e.args[0]) - @staticmethod - def remove_milli_seconds(dt: datetime) -> datetime: - """ - Cuts milliseconds of - :param dt: The time with milliseconds at the end - :return: datetime - """ - - return datetime.strptime(dt.strftime("%Y-%m-%d %H:%M:%S"), "%Y-%m-%d %H:%M:%S") + return out @staticmethod def get_offset(con: Constant, offset) -> RelativeDateTime: From d9f8a56267a1049d7fa5ed0d831fd1f223669286 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Wed, 22 Jun 2022 19:13:34 +0200 Subject: [PATCH 10/57] Fixed evaluator-bug returning wrong year --- datetimeparser/datetimeparser.py | 2 +- datetimeparser/evaluator/evaluatormethods.py | 8 ++++---- datetimeparser/evaluator/evaluatorutils.py | 5 ++++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index 65a6727..ac9c485 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -3,7 +3,7 @@ """ __all__ = ['parse', '__version__', '__author__'] -__version__ = "0.13.1" +__version__ = "0.13.2" __author__ = "aridevelopment" import datetime diff --git a/datetimeparser/evaluator/evaluatormethods.py b/datetimeparser/evaluator/evaluatormethods.py index 9e526ae..05997c5 100644 --- a/datetimeparser/evaluator/evaluatormethods.py +++ b/datetimeparser/evaluator/evaluatormethods.py @@ -46,7 +46,7 @@ def evaluate_constant_relatives(self) -> datetime: ev_out = datetime(base.year, base.month, base.day, hour, minute, sec) elif isinstance(sanitized[-1], RelativeDateTime): - base = self.add_relative_delta(base, sanitized[-1]) + base = self.add_relative_delta(base, sanitized[-1], self.current_time) if sanitized[-2] in WeekdayConstants.ALL: base = self.cut_time(base) @@ -93,7 +93,7 @@ def evaluate_absolute_prepositions(self) -> datetime: sanitized = self.sanitize_input(self.current_time, self.parsed) base = self.get_base(sanitized, base_year, self.current_time) rel_out = self.calc_relative_time(sanitized) - base = self.add_relative_delta(base, rel_out) + base = self.add_relative_delta(base, rel_out, self.current_time) return base @@ -140,14 +140,14 @@ def evaluate_constants(self) -> datetime: ) if object_type.offset: - ev_out = self.add_relative_delta(ev_out, self.get_offset(object_type, self.offset)) + ev_out = self.add_relative_delta(ev_out, self.get_offset(object_type, self.offset), self.current_time) return ev_out def evaluate_relative_datetime(self) -> datetime: out: datetime = self.current_time - out = self.add_relative_delta(out, self.parsed) + out = self.add_relative_delta(out, self.parsed, self.current_time) ev_out = datetime( out.year, out.month, out.day, out.hour, out.minute, out.second ) diff --git a/datetimeparser/evaluator/evaluatorutils.py b/datetimeparser/evaluator/evaluatorutils.py index e5d1ea1..af0cbfe 100644 --- a/datetimeparser/evaluator/evaluatorutils.py +++ b/datetimeparser/evaluator/evaluatorutils.py @@ -182,11 +182,12 @@ def calc_relative_time(sanitized_list: list) -> RelativeDateTime: return ev_out @staticmethod - def add_relative_delta(base_time: datetime, rel_time: RelativeDateTime) -> datetime: + def add_relative_delta(base_time: datetime, rel_time: RelativeDateTime, current_time: datetime) -> datetime: """ Prepares a RelativeDateTime-object for adding to a datetime :param base_time: DateTime-object the time should be added too :param rel_time: RelativeDateTime-object + :param current_time: current datetime :return: relativedelta """ @@ -201,6 +202,8 @@ def add_relative_delta(base_time: datetime, rel_time: RelativeDateTime) -> datet ) try: + if base_time > current_time > base_time + rel: + rel.years += 1 out = base_time + rel except ValueError as e: raise InvalidValue(e.args[0]) From 4a3c5db5a0d321b442d84b58a7154be1375c67ce Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Fri, 29 Jul 2022 18:01:22 +0200 Subject: [PATCH 11/57] Fixed bug where 'next ' raises an error, if the wekday is the current day --- datetimeparser/evaluator/evaluatormethods.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/datetimeparser/evaluator/evaluatormethods.py b/datetimeparser/evaluator/evaluatormethods.py index 05997c5..88ba865 100644 --- a/datetimeparser/evaluator/evaluatormethods.py +++ b/datetimeparser/evaluator/evaluatormethods.py @@ -132,9 +132,12 @@ def evaluate_constants(self) -> datetime: second=dt[2] ) - if self.current_time > dt and self.parsed[0] not in Constants.ALL_RELATIVE_CONSTANTS: + if self.current_time >= dt and self.parsed[0] not in (Constants.ALL_RELATIVE_CONSTANTS and WeekdayConstants.ALL): dt = object_type.time_value(self.current_time.year + 1) + if self.current_time >= dt and self.parsed[0] in WeekdayConstants.ALL: + dt += relativedelta(days=7) + ev_out = datetime( dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second ) From 73fd635140946801a1aabb4321c863492e40863a Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Wed, 10 Aug 2022 13:19:09 +0200 Subject: [PATCH 12/57] Fixed bug in evaluator, using the wrong year for 'first of x' if date has already passed --- datetimeparser/evaluator/evaluatorutils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/datetimeparser/evaluator/evaluatorutils.py b/datetimeparser/evaluator/evaluatorutils.py index af0cbfe..72c04bc 100644 --- a/datetimeparser/evaluator/evaluatorutils.py +++ b/datetimeparser/evaluator/evaluatorutils.py @@ -150,8 +150,11 @@ def get_base(sanitized_input: list, year: int, current_time: datetime) -> dateti if isinstance(sanitized_input[-2], int): dt: datetime = sanitized_input[-1].time_value(year) day: int = sanitized_input[-2] - - return datetime(dt.year, dt.month, day, dt.hour, dt.minute, dt.second) + out = datetime(dt.year, dt.month, day, dt.hour, dt.minute, dt.second) + if out > current_time: + return out + out += relativedelta(years=1) + return out # 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: From 1dc20b2d3a2845df8ed3ae03de83dc70a4a149e2 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Wed, 10 Aug 2022 17:35:03 +0200 Subject: [PATCH 13/57] Removed duplication in .gitignore --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index c415531..88c3bf9 100644 --- a/.gitignore +++ b/.gitignore @@ -134,6 +134,3 @@ dmypy.json # Pyre type checker .pyre/ - -/.idea -.idea/ \ No newline at end of file From b9ba81e543850125605440de7138a82d5bd48f7a Mon Sep 17 00:00:00 2001 From: Ari24-cb24 Date: Tue, 23 Aug 2022 11:14:11 +0200 Subject: [PATCH 14/57] Fixed arguments longer than 2 words in cutoff_words couldn't be recognized in ConstantsParser and ConstantRelativeExtensionsParser --- datetimeparser/parser/parsermethods.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/datetimeparser/parser/parsermethods.py b/datetimeparser/parser/parsermethods.py index 24d2e81..eea4453 100644 --- a/datetimeparser/parser/parsermethods.py +++ b/datetimeparser/parser/parsermethods.py @@ -250,12 +250,12 @@ def parse(self, string: str) -> Optional[Tuple[MethodEnum, Tuple]]: # noqa: C90 :param string: The string to parse :return: A tuple containing the method and the data or None """ - string = string.lower() + string = " ".join(string.lower().split()) # Cut off 'at', 'at the' and 'the' for cutoff_word in self.CUTOFF_KEYWORDS: - if string.split()[0] == cutoff_word: - string = " ".join(string.split()[1:]) + if string.split()[:len(cutoff_word.split())] == cutoff_word.split(): + string = " ".join(string.split()[len(cutoff_word.split()):]) # Strip whitespace string = " ".join(string.split()) @@ -424,8 +424,8 @@ def _get_preposition_keyword(cls, string: str) -> Optional[Tuple[str, str, Union """ # Cut off 'at', 'at the' and 'the' for cutoff_word in cls.CUTOFF_KEYWORDS: - if string.split()[0] == cutoff_word: - string = " ".join(string.split()[1:]) + if string.split()[:len(cutoff_word.split())] == cutoff_word.split(): + string = " ".join(string.split()[len(cutoff_word.split()):]) preposition, string = cls._get_preposition(string) From 8fd611e0a6b2c96451e9668ac80e35c5a47a1329 Mon Sep 17 00:00:00 2001 From: Ari24-cb24 Date: Tue, 23 Aug 2022 11:14:44 +0200 Subject: [PATCH 15/57] Added "after" cutoff word to ConstantsParser to fix a parsing issue with #45 --- 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 eea4453..25d9eac 100644 --- a/datetimeparser/parser/parsermethods.py +++ b/datetimeparser/parser/parsermethods.py @@ -225,7 +225,7 @@ class ConstantsParser: FUTURE_PREPOSITIONS = ("next", "this") # Order is important because "at" and "the" are both in "at the" - CUTOFF_KEYWORDS = ("at the", "in the", "at", "the") + CUTOFF_KEYWORDS = ("at the", "in the", "at", "the", "after") def _find_constant(self, argument: str) -> Optional[Constant]: """ From 411d9c7e36d519036a622bdc76fddade3af33c8a Mon Sep 17 00:00:00 2001 From: Ari24-cb24 Date: Tue, 23 Aug 2022 11:15:05 +0200 Subject: [PATCH 16/57] Added testcases without validation for #45 --- tests/testcases.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/testcases.py b/tests/testcases.py index 0443e8c..df86858 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -122,6 +122,8 @@ def __new__( "next three months": Expected(now=True, delta=relativedelta(months=3)), "today": Expected(), "now": Expected(now=True), + # GitHub issue #45 + "after lunchtime": None }, # Constants "constants": { @@ -156,6 +158,9 @@ def __new__( # GitHub issue #176 "piday": Expected(time_sensitive=True, month=3, day=14), "tauday": Expected(time_sensitive=True, month=6, day=28), + # GitHub issue #45 + "in the morning": None, + "in the evening": None, }, # Constant Relative Extensions "constants_relative_expressions": { From 767e559e0dff218a77dc9c48688c9a4baa51c61f Mon Sep 17 00:00:00 2001 From: Ari24-cb24 Date: Tue, 23 Aug 2022 11:15:30 +0200 Subject: [PATCH 17/57] Fixed testrunner issue where exceptions and returned none where swapped for the parser in the result --- tests/runtests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/runtests.py b/tests/runtests.py index 4c9bc8b..bfc8600 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -149,8 +149,8 @@ def evaluate_testcases(testcase_results: dict, disable_color=False, disable_inde print(f"{Colors.ANSI_YELLOW}No validation tests: {Colors.ANSI_BOLD_WHITE}{overall_results[StatusType.NO_VALIDATION]}/{len_testcases}") print(f"{Colors.ANSI_RED}Wrong result tests: {Colors.ANSI_BOLD_WHITE}{overall_results[StatusType.WRONG_RESULT]}/{len_testcases}") print() - print(f"{Colors.ANSI_RED}Parser returned None: {Colors.ANSI_BOLD_WHITE}{overall_results[StatusType.PARSER_EXCEPTION]}/{len_testcases}") - print(f"{Colors.ANSI_LIGHT_RED}{Colors.ANSI_UNDERLINE}Parser exceptions: {Colors.ANSI_RESET}{Colors.ANSI_BOLD_WHITE}{overall_results[StatusType.PARSER_RETURNS_NONE]}/{len_testcases}") + print(f"{Colors.ANSI_RED}Parser returned None: {Colors.ANSI_BOLD_WHITE}{overall_results[StatusType.PARSER_RETURNS_NONE]}/{len_testcases}") + print(f"{Colors.ANSI_LIGHT_RED}{Colors.ANSI_UNDERLINE}Parser exceptions: {Colors.ANSI_RESET}{Colors.ANSI_BOLD_WHITE}{overall_results[StatusType.PARSER_EXCEPTION]}/{len_testcases}") print(f"{Colors.ANSI_RED}Evaluator returned None: {Colors.ANSI_BOLD_WHITE}{overall_results[StatusType.EVALUATOR_RETURNS_NONE]}/{len_testcases}") print(f"{Colors.ANSI_LIGHT_RED}{Colors.ANSI_UNDERLINE}Evaluator exceptions: {Colors.ANSI_RESET}{Colors.ANSI_BOLD_WHITE}{overall_results[StatusType.EVALUATOR_EXCEPTION]}/{len_testcases}") else: From dbad71fa828a0b2ccf3a2187bd8662440bf226d5 Mon Sep 17 00:00:00 2001 From: Ari24-cb24 Date: Tue, 23 Aug 2022 11:18:47 +0200 Subject: [PATCH 18/57] Bump of version number to 0.13.3 --- datetimeparser/datetimeparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index ac9c485..dba6cda 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -3,7 +3,7 @@ """ __all__ = ['parse', '__version__', '__author__'] -__version__ = "0.13.2" +__version__ = "0.13.3" __author__ = "aridevelopment" import datetime From f073eaec15593ad3facdaf10c49e5a854c3ebb61 Mon Sep 17 00:00:00 2001 From: Ari24-cb24 Date: Tue, 23 Aug 2022 11:26:00 +0200 Subject: [PATCH 19/57] Fixed linting issues --- datetimeparser/parser/parsermethods.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/datetimeparser/parser/parsermethods.py b/datetimeparser/parser/parsermethods.py index 25d9eac..721aa5e 100644 --- a/datetimeparser/parser/parsermethods.py +++ b/datetimeparser/parser/parsermethods.py @@ -771,7 +771,10 @@ def _parse_relative_statement(self, relative_statement: str, preposition: str) - return returned_data - def _concatenate_relative_data(self, relative_data_tokens: List[Union[int, Constant]], preposition: str) -> List[Union[int, Constant, RelativeDateTime]]: + def _concatenate_relative_data( + self, relative_data_tokens: List[Union[int, Constant]], + preposition: str + ) -> List[Union[int, Constant, RelativeDateTime]]: """ Concatenates [1, RelativeDate(DAY), 2, RelativeDate(MONTH)] into [RelativeDate(days=1, months=2)] respecting the preposition (future and past) @@ -829,7 +832,9 @@ def _parse_absolute_statement(self, data: Union[str, Tuple]) -> Optional: if isinstance(data, str): # Try constants (e.g. "(three days after) christmas") constants_parser = ConstantsParser() - constants_parser.CONSTANT_KEYWORDS = (*Constants.ALL, *DatetimeDeltaConstants.ALL, *DatetimeConstants.ALL, *MonthConstants.ALL, *WeekdayConstants.ALL) + constants_parser.CONSTANT_KEYWORDS = ( + *Constants.ALL, *DatetimeDeltaConstants.ALL, *DatetimeConstants.ALL, *MonthConstants.ALL, *WeekdayConstants.ALL + ) constants_parser.PREPOSITIONS = ("last", "next", "this", "previous") constants_parser.PAST_PREPOSITIONS = ("last", "previous") constants_parser.FUTURE_PREPOSITIONS = ("next", "this") From ae073d6a5939c164f12ae7b8480d044f07dc495c Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sun, 11 Sep 2022 08:35:13 +0200 Subject: [PATCH 20/57] Fixed bug with TimeConstants (f.e. 'in the morning') --- datetimeparser/evaluator/evaluatormethods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datetimeparser/evaluator/evaluatormethods.py b/datetimeparser/evaluator/evaluatormethods.py index 88ba865..392cdcb 100644 --- a/datetimeparser/evaluator/evaluatormethods.py +++ b/datetimeparser/evaluator/evaluatormethods.py @@ -121,7 +121,6 @@ def evaluate_constants(self) -> datetime: else: dt = object_type.time_value(self.current_time.year) - if isinstance(dt, tuple): dt = datetime( year=self.current_time.year, @@ -131,6 +130,7 @@ def evaluate_constants(self) -> datetime: minute=dt[1], second=dt[2] ) + return dt if self.current_time >= dt and self.parsed[0] not in (Constants.ALL_RELATIVE_CONSTANTS and WeekdayConstants.ALL): dt = object_type.time_value(self.current_time.year + 1) From dca96dc046c602a333be117eedb3ec99f07f2a54 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sun, 11 Sep 2022 10:09:58 +0200 Subject: [PATCH 21/57] Fixed bug with 'of' (f.e. 'x week of y') --- datetimeparser/evaluator/evaluatormethods.py | 7 ++- datetimeparser/evaluator/evaluatorutils.py | 46 +++++++++++++++----- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/datetimeparser/evaluator/evaluatormethods.py b/datetimeparser/evaluator/evaluatormethods.py index 392cdcb..ac42247 100644 --- a/datetimeparser/evaluator/evaluatormethods.py +++ b/datetimeparser/evaluator/evaluatormethods.py @@ -90,8 +90,11 @@ def evaluate_constant_relatives(self) -> datetime: def evaluate_absolute_prepositions(self) -> datetime: base_year = self.current_time.year - sanitized = self.sanitize_input(self.current_time, self.parsed) - base = self.get_base(sanitized, base_year, self.current_time) + sanitized, given_year = self.sanitize_input(self.current_time, self.parsed) + if not given_year: + base = self.get_base(sanitized, base_year, self.current_time) + else: + base = self.get_base(sanitized, given_year, self.current_time, forced=True) rel_out = self.calc_relative_time(sanitized) base = self.add_relative_delta(base, rel_out, self.current_time) diff --git a/datetimeparser/evaluator/evaluatorutils.py b/datetimeparser/evaluator/evaluatorutils.py index 72c04bc..332d437 100644 --- a/datetimeparser/evaluator/evaluatorutils.py +++ b/datetimeparser/evaluator/evaluatorutils.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Any, Union from datetimeparser.utils.baseclasses import * from datetimeparser.utils.enums import * @@ -20,6 +20,19 @@ def get_week_of(dt: datetime) -> datetime: return dt + timedelta(days=(7 - dt.weekday())) + @staticmethod + def x_week_of_month(relative_dt: RelativeDateTime, idx: int, parsed: list[Any, ...], year): + + parsed[idx + 1] = EvaluatorUtils.datetime_to_absolute_datetime(parsed[idx + 1].time_value(year)) + + relative_dt.days = EvaluatorUtils.get_week_of( + EvaluatorUtils.absolute_datetime_to_datetime(parsed[idx + 1]) + ).day - 1 + + relative_dt.weeks -= 1 + + return parsed + @staticmethod def absolute_datetime_to_datetime(absolute_datetime: AbsoluteDateTime) -> datetime: """ @@ -59,7 +72,9 @@ def datetime_to_absolute_datetime(dt: datetime) -> AbsoluteDateTime: return absdt @staticmethod - def sanitize_input(current_time: datetime, parsed_list: list) -> List[Union[RelativeDateTime, AbsoluteDateTime, int, Constant]]: + def sanitize_input( + current_time: datetime, parsed_list: list + ) -> tuple[List[Union[RelativeDateTime, AbsoluteDateTime, int, Constant]], int]: """ Removes useless keywords :param parsed_list: The list that should be sanitized @@ -67,6 +82,7 @@ def sanitize_input(current_time: datetime, parsed_list: list) -> List[Union[Rela :return: a list without keywords """ + given_year = 0 for idx, element in enumerate(parsed_list): if isinstance(element, Constant) and element.name == "of": if isinstance(parsed_list[idx - 1], RelativeDateTime): @@ -81,17 +97,21 @@ def sanitize_input(current_time: datetime, parsed_list: list) -> List[Union[Rela if parsed_list[idx + 1] in MonthConstants.ALL: try: year = parsed_list.pop(idx + 2).year + given_year = year except IndexError: year = current_time.year - parsed_list[idx + 1] = EvaluatorUtils.datetime_to_absolute_datetime(parsed_list[idx + 1].time_value(year)) - relative_dt.days = EvaluatorUtils.get_week_of( - EvaluatorUtils.absolute_datetime_to_datetime(parsed_list[idx + 1]) - ).day - 1 + pars1, pars2 = parsed_list.copy(), parsed_list.copy() + ghost_parsed_list = EvaluatorUtils.x_week_of_month(relative_dt, idx, pars1, year) + test_out = EvaluatorUtils.add_relative_delta( + EvaluatorUtils.absolute_datetime_to_datetime(ghost_parsed_list[-1]), + ghost_parsed_list[0], + current_time + ) + if current_time > test_out and not given_year: + parsed_list = EvaluatorUtils.x_week_of_month(relative_dt, idx, pars2, year+1) - relative_dt.weeks -= 1 - - return list(filter(lambda e: e not in Keywords.ALL and not isinstance(e, str), parsed_list)) + return list(filter(lambda e: e not in Keywords.ALL and not isinstance(e, str), parsed_list)), given_year @staticmethod def cut_time(time: datetime) -> datetime: @@ -104,13 +124,14 @@ 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) -> datetime: + def get_base(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 :param sanitized_input: The sanitized list :param year: The year for the Constant :param current_time: The current datetime + :param forced: If a given year should be used regardless of current time :return: datetime """ @@ -151,13 +172,14 @@ def get_base(sanitized_input: list, year: int, current_time: datetime) -> dateti 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: + + if out > current_time or forced: return out out += relativedelta(years=1) return out # 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: + if sanitized_input[-1].time_value(year) > current_time or forced: return sanitized_input[-1].time_value(year) else: return sanitized_input[-1].time_value(year + 1) From 41a131acafbabd7c8c94e97ae067e8fb45683e2f Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sun, 11 Sep 2022 10:47:08 +0200 Subject: [PATCH 22/57] Fixed small mistake caused by wrong return value --- datetimeparser/evaluator/evaluatormethods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datetimeparser/evaluator/evaluatormethods.py b/datetimeparser/evaluator/evaluatormethods.py index ac42247..dc71491 100644 --- a/datetimeparser/evaluator/evaluatormethods.py +++ b/datetimeparser/evaluator/evaluatormethods.py @@ -33,7 +33,7 @@ def evaluate_absolute_date_formats(self) -> datetime: return ev_out def evaluate_constant_relatives(self) -> datetime: - sanitized = self.sanitize_input(self.current_time, self.parsed) + sanitized, _ = self.sanitize_input(self.current_time, self.parsed) base: datetime = self.current_time ev_out = None From 6ade41222a1376af3b4ac7a58bceb86d994d5eb9 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sun, 11 Sep 2022 10:48:22 +0200 Subject: [PATCH 23/57] Changed version number --- datetimeparser/datetimeparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index dba6cda..cc74913 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -3,7 +3,7 @@ """ __all__ = ['parse', '__version__', '__author__'] -__version__ = "0.13.3" +__version__ = "0.13.4" __author__ = "aridevelopment" import datetime From 5fb827c819def036651e092a15281a92faec3915 Mon Sep 17 00:00:00 2001 From: Ari24-cb24 Date: Sun, 11 Sep 2022 11:10:02 +0200 Subject: [PATCH 24/57] Fixed ConstantRelativeExtensionParser not accepting arguments like "tomorrow 12 o'clock" by extending the DatetimeDeltaConstantsParser --- datetimeparser/parser/parsermethods.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/datetimeparser/parser/parsermethods.py b/datetimeparser/parser/parsermethods.py index 721aa5e..359c309 100644 --- a/datetimeparser/parser/parsermethods.py +++ b/datetimeparser/parser/parsermethods.py @@ -378,10 +378,15 @@ def _find_constant(cls, string: str, rest_arguments: List[str]) -> Optional[Tupl return None else: # Only the argument directly after the keyword can be a year + # Except for cases like tomorrow 12 o'clock if rest_arguments[0].isdecimal(): year = int(rest_arguments[0]) result = cls._find_constant(string, []) + # Check the exception via a DatetimeDeltaConstantsParser and set year to None. + if DatetimeDeltaConstantsParser().parse(" ".join(rest_arguments)) is not None: + year = None + if result is not None: return result[0], year else: @@ -443,6 +448,8 @@ def _get_preposition_keyword(cls, string: str) -> Optional[Tuple[str, str, Union result = cls._find_constant(tryable_keyword, arguments[i:]) if result is not None: + # Year might also be a clock time (such as (tomorrow) 12 (o' clock)) + # If that's the case, year will be None keyword, year = result break @@ -510,6 +517,7 @@ def parse(self, string: str) -> Optional[Tuple[Method, Tuple]]: if result is None: return None + # Identify the first parts of the string and cutoff keyword + year if a year exists (there might also be no year) first_preposition, tryable_keyword, first_keyword, first_year, string = result string = string[len(tryable_keyword) + len(str(first_year if first_year is not None else "")):] string = " ".join(string.split()) @@ -596,7 +604,7 @@ def parse(self, string: str) -> Optional[Tuple[MethodEnum, AbsoluteDateTime]]: continue # If the time does not match a clocktime format, does not contain a colon and is a number - # e.g. "3(pm|am)", return that time respecting the after_midday flag + # e.g. "3(pm|am)" or "3 o'clock", return that time respecting the after_midday flag if not parsed_time and time.count(":") == 0 and parse_int(time): if after_midday is not None: parsed_time = AbsoluteDateTime(hour=(12 if after_midday else 0) + int(time)) @@ -620,7 +628,7 @@ def parse(self, string: str) -> Optional[Tuple[MethodEnum, AbsoluteDateTime]]: # If there's no more content left # Return the parsed time - if not data: + if not data or (len(data) == 1 and data[0].lower() == 'o\'clock'): return Method.DATETIME_DELTA_CONSTANTS, parsed_time else: # Otherwise search for constants like From d3b136d111bc116dee1055507ffea2ec9fd14171 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sun, 11 Sep 2022 12:16:28 +0200 Subject: [PATCH 25/57] Fixed typingdown to 3.7 --- datetimeparser/datetimeparser.py | 6 +++--- datetimeparser/evaluator/evaluatorutils.py | 6 +++--- datetimeparser/utils/exceptions.py | 5 ++++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index cc74913..8c19d9e 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -3,17 +3,17 @@ """ __all__ = ['parse', '__version__', '__author__'] -__version__ = "0.13.4" +__version__ = "0.14.4" __author__ = "aridevelopment" import datetime -from typing import Union +from typing import Optional from datetimeparser.evaluator import Evaluator from datetimeparser.parser import Parser -def parse(datetime_string: str, timezone: str = "Europe/Berlin") -> Union[datetime.datetime, None]: +def parse(datetime_string: str, timezone: str = "Europe/Berlin") -> Optional[datetime.datetime]: """ Parses a datetime string and returns a datetime object. If the datetime string cannot be parsed, None is returned. diff --git a/datetimeparser/evaluator/evaluatorutils.py b/datetimeparser/evaluator/evaluatorutils.py index 332d437..f769fca 100644 --- a/datetimeparser/evaluator/evaluatorutils.py +++ b/datetimeparser/evaluator/evaluatorutils.py @@ -1,4 +1,4 @@ -from typing import Any, Union +from typing import Any, Tuple, Union from datetimeparser.utils.baseclasses import * from datetimeparser.utils.enums import * @@ -21,7 +21,7 @@ def get_week_of(dt: datetime) -> datetime: return dt + timedelta(days=(7 - dt.weekday())) @staticmethod - def x_week_of_month(relative_dt: RelativeDateTime, idx: int, parsed: list[Any, ...], year): + def x_week_of_month(relative_dt: RelativeDateTime, idx: int, parsed: List[Union[Any]], year): parsed[idx + 1] = EvaluatorUtils.datetime_to_absolute_datetime(parsed[idx + 1].time_value(year)) @@ -74,7 +74,7 @@ def datetime_to_absolute_datetime(dt: datetime) -> AbsoluteDateTime: @staticmethod def sanitize_input( current_time: datetime, parsed_list: list - ) -> tuple[List[Union[RelativeDateTime, AbsoluteDateTime, int, Constant]], int]: + ) -> Tuple[List[Union[RelativeDateTime, AbsoluteDateTime, int, Constant]], int]: """ Removes useless keywords :param parsed_list: The list that should be sanitized diff --git a/datetimeparser/utils/exceptions.py b/datetimeparser/utils/exceptions.py index ded9de0..a569238 100644 --- a/datetimeparser/utils/exceptions.py +++ b/datetimeparser/utils/exceptions.py @@ -1,3 +1,6 @@ +from typing import Union + + class EvaluatorException(Exception): """ Base class for Evaluator Exceptions @@ -12,7 +15,7 @@ def __str__(self): class FailedEvaluation(EvaluatorException): - def __init__(self, tried: str | list = None): + def __init__(self, tried: Union[str, list, None] = None): super().__init__(type(self).__name__, f"Cannot evaluate {tried} into datetime") From fabfb4d13f0f2adf9361d93e41297b09adbfcdf6 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sun, 11 Sep 2022 12:24:49 +0200 Subject: [PATCH 26/57] Fixed version number --- datetimeparser/datetimeparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index 8c19d9e..810b2d5 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -3,7 +3,7 @@ """ __all__ = ['parse', '__version__', '__author__'] -__version__ = "0.14.4" +__version__ = "0.13.5" __author__ = "aridevelopment" import datetime From 512d3fcdbe2c1e3be9a745c12d0f7d405bddcf3f Mon Sep 17 00:00:00 2001 From: Ari van Houten <56089155+Ari24-cb24@users.noreply.github.com> Date: Sun, 11 Sep 2022 12:27:59 +0200 Subject: [PATCH 27/57] Fixed matrix strategy failing-fast --- .github/workflows/python-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 86aa4d1..522ac6a 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -15,6 +15,7 @@ jobs: build: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: python-version: ["3.7", "3.8", "3.9", "3.10"] From 1cb9496c9139030a07eec67da77e546daef90a6b Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Mon, 3 Oct 2022 12:15:17 +0200 Subject: [PATCH 28/57] Added formula for calulationg sunrise/sunset --- datetimeparser/utils/formulars.py | 48 +++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/datetimeparser/utils/formulars.py b/datetimeparser/utils/formulars.py index 3547977..e04314e 100644 --- a/datetimeparser/utils/formulars.py +++ b/datetimeparser/utils/formulars.py @@ -1,4 +1,13 @@ from datetime import datetime, timedelta +import math + + +def day_of_year(dt: datetime) -> int: + n1 = math.floor(275 * dt.month / 9) + n2 = math.floor((dt.month + 9) / 12) + n3 = (1 + math.floor((dt.year - 4 * math.floor(dt.year / 4) + 2) / 3)) + + return n1 - (n2 * n3) + dt.day - 30 def eastern_calc(year_time: int) -> datetime: @@ -34,3 +43,42 @@ def days_feb(year_time: int) -> int: def year_start(year_time: int) -> datetime: return datetime(year=year_time, month=1, day=1) + + +def calc_sun_time(dt: datetime, timezone: tuple[float, float, float], sunrise: bool = True) -> datetime: + """ + Calculates the time for sunrise and sunset based on coordinates and a date + :param dt: The date for calculating the sunset + :param timezone: A tuple with longitude and magnitude and timezone offset + :param sunrise: If True the sunrise will be calculated if False the sunset + :returns: The time for the sunrise/sunset + """ + + to_rad: float = math.pi / 180 + day: int = day_of_year(dt) + longitude_to_hour = timezone[0] / 15 + + b = timezone[1] * to_rad + h = -50 * to_rad / 60 + + time_equation = -0.171 * math.sin(0.0337 * day + 0.465) - 0.1299 * math.sin(0.01787 * day - 0.168) + declination = 0.4095 * math.sin(0.016906 * (day - 80.086)) + + time_difference = 12 * math.acos((math.sin(h) - math.sin(b) * math. sin(declination)) / (math.cos(b) * math.cos(declination))) / math.pi + + if sunrise: + time = 12 - time_difference + else: + time = 12 + time_difference + + time: float = (time - time_equation) + longitude_to_hour + timezone[2] + + hour: int = int(time) + minutes_left: float = time - int(time) + minutes_with_seconds = minutes_left * 60 + minute: int = int(minutes_with_seconds) + second: int = int((minutes_with_seconds - minute) * 60) + + out: datetime = datetime(year=dt.year, month=dt.month, day=dt.day, hour=hour, minute=minute, second=second) + + return out From c6749f6748c24854b7054609e8aa023aed6108db Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 27 Oct 2022 14:13:16 +0200 Subject: [PATCH 29/57] Started to implement coordinates for sunrise/sunset calculation (not functional atm) --- datetimeparser/datetimeparser.py | 13 +++++++++--- datetimeparser/evaluator/evaluator.py | 16 +++++++++----- datetimeparser/evaluator/evaluatormethods.py | 18 ++++++++++++++-- datetimeparser/utils/enums.py | 7 +++++-- datetimeparser/utils/formulars.py | 2 +- datetimeparser/utils/geometry.py | 22 ++++++++++++++++++++ requirements.txt | 3 ++- tests/runtests.py | 5 +++-- 8 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 datetimeparser/utils/geometry.py diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index 810b2d5..540e83b 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -13,13 +13,20 @@ from datetimeparser.parser import Parser -def parse(datetime_string: str, timezone: str = "Europe/Berlin") -> Optional[datetime.datetime]: +def parse( + datetime_string: str, + timezone: str = "Europe/Berlin", + coordinates: Optional[tuple[float, float]] = None +) -> Optional[tuple[datetime.datetime, str, tuple[float, float]]]: """ Parses a datetime string and returns a datetime object. If the datetime string cannot be parsed, None is returned. :param datetime_string: The datetime string to parse. :param timezone: The timezone to use. Should be a valid timezone for pytz.timezone(). Default: Europe/Berlin + :param coordinates: A tuple containing longitude and latitude. If coordinates are given, the timezone will be calculated, + independently of the given timezone param + NOTE: It takes a longer time to calculate the timezone, it can happen that it takes up to 30 seconds for a result :return: A datetime object or None """ parser_result = Parser(datetime_string).parse() @@ -27,9 +34,9 @@ def parse(datetime_string: str, timezone: str = "Europe/Berlin") -> Optional[dat if parser_result is None: return None - evaluator_result = Evaluator(parser_result, tz=timezone).evaluate() + evaluator_result, tz, coords = Evaluator(parser_result, tz=timezone, coordinates=coordinates).evaluate() if evaluator_result is None: return None - return evaluator_result + return evaluator_result, tz, coords diff --git a/datetimeparser/evaluator/evaluator.py b/datetimeparser/evaluator/evaluator.py index 50980d9..3f7bc4e 100644 --- a/datetimeparser/evaluator/evaluator.py +++ b/datetimeparser/evaluator/evaluator.py @@ -1,20 +1,24 @@ from datetime import datetime from pytz import timezone, UnknownTimeZoneError -from typing import Union +from typing import Optional, Union from datetimeparser.utils.baseclasses import AbsoluteDateTime, RelativeDateTime from datetimeparser.utils.enums import Method from datetimeparser.evaluator.evaluatormethods import EvaluatorMethods from datetimeparser.utils.exceptions import FailedEvaluation, InvalidValue +from datetimeparser.utils.geometry import TimeZoneManager class Evaluator: - def __init__(self, parsed_object, tz="Europe/Berlin"): + def __init__(self, parsed_object, tz="Europe/Berlin", coordinates: Optional[tuple[float, float]] = None): """ :param parsed_object: the parsed object from parser :param tz: the timezone for the datetime + :param coordinates: longitude and latitude for timezone calculation and for sunrise and sunset """ + if coordinates: + tz = TimeZoneManager().timezone_at(lng=coordinates[0], lat=coordinates[1]) try: tiz = timezone(tz) except UnknownTimeZoneError: @@ -24,10 +28,12 @@ def __init__(self, parsed_object, tz="Europe/Berlin"): self.parsed_object_content: Union[list, AbsoluteDateTime, RelativeDateTime] = parsed_object[1] self.current_datetime: datetime = datetime.strptime(datetime.strftime(datetime.now(tz=tiz), "%Y-%m-%d %H:%M:%S"), "%Y-%m-%d %H:%M:%S") self.offset = tiz.utcoffset(self.current_datetime) + self.timezone = tiz + self.coordinates = coordinates or TimeZoneManager().get_coordinates(tiz.zone) - def evaluate(self) -> Union[datetime, None]: + def evaluate(self) -> Union[tuple[datetime, str, tuple[float, float]], None]: ev_out = None - ev = EvaluatorMethods(self.parsed_object_content, self.current_datetime, self.offset) + ev = EvaluatorMethods(self.parsed_object_content, self.current_datetime, self.coordinates, self.offset) if self.parsed_object_type == Method.ABSOLUTE_DATE_FORMATS: ev_out = ev.evaluate_absolute_date_formats() @@ -48,6 +54,6 @@ def evaluate(self) -> Union[datetime, None]: ev_out = ev.evaluate_datetime_delta_constants() if ev_out: - return ev_out + return ev_out, self.timezone.zone, self.coordinates else: raise FailedEvaluation(self.parsed_object_content) diff --git a/datetimeparser/evaluator/evaluatormethods.py b/datetimeparser/evaluator/evaluatormethods.py index dc71491..1130edf 100644 --- a/datetimeparser/evaluator/evaluatormethods.py +++ b/datetimeparser/evaluator/evaluatormethods.py @@ -2,6 +2,7 @@ from datetimeparser.utils.baseclasses import * from datetimeparser.utils.enums import * from datetimeparser.utils.exceptions import InvalidValue +from datetimeparser.utils.formulars import calc_sun_time class EvaluatorMethods(EvaluatorUtils): @@ -9,16 +10,18 @@ class EvaluatorMethods(EvaluatorUtils): Evaluates a datetime-object from a given list returned from the parser """ - def __init__(self, parsed, current_time: datetime, offset: timedelta = None): + def __init__(self, parsed, current_time: datetime, coordinates: tuple[float, float], offset: timedelta = None): """ :param parsed: object returned from the parser :param current_time: the current datetime + :param coordinates: coordinates from the timezone :param offset: the UTC-offset from the current timezone. Default: None """ self.parsed = parsed self.current_time = current_time self.offset = offset + self.coordinates = coordinates def evaluate_absolute_date_formats(self) -> datetime: ev_out = datetime( @@ -122,6 +125,15 @@ def evaluate_constants(self) -> datetime: "%Y-%m-%d %H:%M:%S" ) + elif object_type.name == "sunset" or object_type.name == "sunrise": + ofs = self.offset.total_seconds()/60/60 # -> to hours + # TODO: at the moment summer and winter time change the result for the offset around 1 hour + dt = calc_sun_time( + self.current_time, + (self.coordinates[0], self.coordinates[1], ofs), # something does not work properly, Berlin works with offset 0, Dubai not, idk why + object_type.name == "sunrise" + ) + else: dt = object_type.time_value(self.current_time.year) if isinstance(dt, tuple): @@ -135,7 +147,9 @@ def evaluate_constants(self) -> datetime: ) return dt - if self.current_time >= dt and self.parsed[0] not in (Constants.ALL_RELATIVE_CONSTANTS and WeekdayConstants.ALL): + if self.current_time >= dt and self.parsed[0] not in ( + Constants.ALL_RELATIVE_CONSTANTS and WeekdayConstants.ALL and DatetimeDeltaConstants.CHANGING + ): dt = object_type.time_value(self.current_time.year + 1) if self.current_time >= dt and self.parsed[0] in WeekdayConstants.ALL: diff --git a/datetimeparser/utils/enums.py b/datetimeparser/utils/enums.py index 5404418..721f54f 100644 --- a/datetimeparser/utils/enums.py +++ b/datetimeparser/utils/enums.py @@ -119,7 +119,7 @@ class DatetimeDeltaConstants: DAYLIGHT_CHANGE = Constant('daylight change', ['daylight saving', 'daylight saving time'], value=0, options=[ConstantOption.YEAR_VARIABLE, ConstantOption.DATE_VARIABLE], time_value=lambda _: (6, 0, 0)) - SUNRISE = Constant('sunrise', value=0, options=[ConstantOption.DATE_VARIABLE], time_value=lambda _: (7, 0, 0)) + SUNRISE = Constant('sunrise', value=0, options=[ConstantOption.DATE_VARIABLE], time_value=lambda _: None) MORNING = Constant('morning', value=0, options=[ConstantOption.DATE_VARIABLE], time_value=lambda _: (6, 0, 0)) BREAKFAST = Constant('breakfast', value=0, options=[ConstantOption.DATE_VARIABLE], time_value=lambda _: (8, 0, 0)) @@ -132,12 +132,15 @@ class DatetimeDeltaConstants: time_value=lambda _: (19, 0, 0)) DAWN = Constant('dawn', value=12, options=[ConstantOption.DATE_VARIABLE], time_value=lambda _: (6, 0, 0)) DUSK = Constant('dusk', value=12, options=[ConstantOption.DATE_VARIABLE], time_value=lambda _: (20, 0, 0)) - SUNSET = Constant('sunset', value=12, options=[ConstantOption.DATE_VARIABLE], time_value=lambda _: (18, 30, 0)) + SUNSET = Constant('sunset', value=12, options=[ConstantOption.DATE_VARIABLE], time_value=lambda _: None) ALL = [ MORNING, AFTERNOON, EVENING, NIGHT, MORNING_NIGHT, DAYLIGHT_CHANGE, MIDNIGHT, MIDDAY, DAWN, DUSK, SUNRISE, SUNSET, LUNCH, DINNER, BREAKFAST ] + CHANGING = [ + SUNRISE, SUNSET + ] class NumberConstants: diff --git a/datetimeparser/utils/formulars.py b/datetimeparser/utils/formulars.py index e04314e..740c1b9 100644 --- a/datetimeparser/utils/formulars.py +++ b/datetimeparser/utils/formulars.py @@ -49,7 +49,7 @@ def calc_sun_time(dt: datetime, timezone: tuple[float, float, float], sunrise: b """ Calculates the time for sunrise and sunset based on coordinates and a date :param dt: The date for calculating the sunset - :param timezone: A tuple with longitude and magnitude and timezone offset + :param timezone: A tuple with longitude and latitude and timezone offset :param sunrise: If True the sunrise will be calculated if False the sunset :returns: The time for the sunrise/sunset """ diff --git a/datetimeparser/utils/geometry.py b/datetimeparser/utils/geometry.py new file mode 100644 index 0000000..c70e284 --- /dev/null +++ b/datetimeparser/utils/geometry.py @@ -0,0 +1,22 @@ +from timezonefinder import TimezoneFinder + + +class TimeZoneManager(TimezoneFinder): + + def __init__(self): + super(TimeZoneManager, self).__init__(in_memory=True) + + def get_coordinates(self, timezone: str) -> tuple[float, float]: + coords = self.get_geometry(tz_name=timezone, coords_as_pairs=True) + + while not isinstance(coords[0], tuple): + coords = coords[len(coords)//2] + + coords: tuple[float, float] = coords[len(coords)//2] + + # timezone = self.timezone_at(lng=coords[0] + 1, lat=coords[1]) + # TODO: needs to be improved, at the moment it's just a small fix, not tested if it works with all timezones + # TODO: add testcases for ALL timezones if possible to check if the "+1" fix is working + # at the moment it returns "Europe/Belgium" if the timezone "Europe/Berlin" is used -> the "+1" on longitude fixes that + + return coords[0] + 1, coords[1] diff --git a/requirements.txt b/requirements.txt index 0b65956..a1e6ab3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ python-dateutil pytz -typing \ No newline at end of file +typing +timezonefinder diff --git a/tests/runtests.py b/tests/runtests.py index bfc8600..02e9ee3 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -45,10 +45,11 @@ def get_testcase_results(testcase: str, expected_value: datetime.datetime = None if parser_result is None: return StatusType.PARSER_RETURNS_NONE, None - evaluator = Evaluator(parser_result, tz="Europe/Berlin") + evaluator = Evaluator(parser_result, tz="Europe/Berlin", coordinates=(13.41053, 52.52437)) + # Berlin (13.41053, 52.52437), Dubai (55.2962, 25.2684) try: - evaluator_result = evaluator.evaluate() + evaluator_result, _, _ = evaluator.evaluate() except BaseException as error: if expected_value == ThrowException: return StatusType.SUCCESS, "Evaluator threw exception but it was expected" From 100ff1067589d58a20db5b8637e625a75e7a3a9b Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 27 Oct 2022 18:09:14 +0200 Subject: [PATCH 30/57] Fixed wrong suntimes, resolved bugs --- datetimeparser/evaluator/evaluator.py | 4 ++-- datetimeparser/evaluator/evaluatormethods.py | 8 ++++++-- datetimeparser/utils/formulars.py | 10 +++++----- tests/runtests.py | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/datetimeparser/evaluator/evaluator.py b/datetimeparser/evaluator/evaluator.py index 3f7bc4e..47c771f 100644 --- a/datetimeparser/evaluator/evaluator.py +++ b/datetimeparser/evaluator/evaluator.py @@ -29,11 +29,11 @@ def __init__(self, parsed_object, tz="Europe/Berlin", coordinates: Optional[tupl self.current_datetime: datetime = datetime.strptime(datetime.strftime(datetime.now(tz=tiz), "%Y-%m-%d %H:%M:%S"), "%Y-%m-%d %H:%M:%S") self.offset = tiz.utcoffset(self.current_datetime) self.timezone = tiz - self.coordinates = coordinates or TimeZoneManager().get_coordinates(tiz.zone) + self.coordinates = coordinates def evaluate(self) -> Union[tuple[datetime, str, tuple[float, float]], None]: ev_out = None - ev = EvaluatorMethods(self.parsed_object_content, self.current_datetime, self.coordinates, self.offset) + ev = EvaluatorMethods(self.parsed_object_content, self.current_datetime, self.coordinates, self.timezone.zone, self.offset) if self.parsed_object_type == Method.ABSOLUTE_DATE_FORMATS: ev_out = ev.evaluate_absolute_date_formats() diff --git a/datetimeparser/evaluator/evaluatormethods.py b/datetimeparser/evaluator/evaluatormethods.py index 1130edf..e3e629d 100644 --- a/datetimeparser/evaluator/evaluatormethods.py +++ b/datetimeparser/evaluator/evaluatormethods.py @@ -3,6 +3,7 @@ from datetimeparser.utils.enums import * from datetimeparser.utils.exceptions import InvalidValue from datetimeparser.utils.formulars import calc_sun_time +from datetimeparser.utils.geometry import TimeZoneManager class EvaluatorMethods(EvaluatorUtils): @@ -10,7 +11,7 @@ class EvaluatorMethods(EvaluatorUtils): Evaluates a datetime-object from a given list returned from the parser """ - def __init__(self, parsed, current_time: datetime, coordinates: tuple[float, float], offset: timedelta = None): + def __init__(self, parsed, current_time: datetime, coordinates: tuple[float, float], timezone: str, offset: timedelta = None): """ :param parsed: object returned from the parser :param current_time: the current datetime @@ -22,6 +23,7 @@ def __init__(self, parsed, current_time: datetime, coordinates: tuple[float, flo self.current_time = current_time self.offset = offset self.coordinates = coordinates + self.timezone = timezone def evaluate_absolute_date_formats(self) -> datetime: ev_out = datetime( @@ -128,9 +130,11 @@ def evaluate_constants(self) -> datetime: elif object_type.name == "sunset" or object_type.name == "sunrise": ofs = self.offset.total_seconds()/60/60 # -> to hours # TODO: at the moment summer and winter time change the result for the offset around 1 hour + if not self.coordinates: + self.coordinates = TimeZoneManager().get_coordinates(self.timezone) dt = calc_sun_time( self.current_time, - (self.coordinates[0], self.coordinates[1], ofs), # something does not work properly, Berlin works with offset 0, Dubai not, idk why + (self.coordinates[0], self.coordinates[1], ofs), object_type.name == "sunrise" ) diff --git a/datetimeparser/utils/formulars.py b/datetimeparser/utils/formulars.py index 740c1b9..cea869d 100644 --- a/datetimeparser/utils/formulars.py +++ b/datetimeparser/utils/formulars.py @@ -57,7 +57,6 @@ def calc_sun_time(dt: datetime, timezone: tuple[float, float, float], sunrise: b to_rad: float = math.pi / 180 day: int = day_of_year(dt) longitude_to_hour = timezone[0] / 15 - b = timezone[1] * to_rad h = -50 * to_rad / 60 @@ -66,12 +65,12 @@ def calc_sun_time(dt: datetime, timezone: tuple[float, float, float], sunrise: b time_difference = 12 * math.acos((math.sin(h) - math.sin(b) * math. sin(declination)) / (math.cos(b) * math.cos(declination))) / math.pi - if sunrise: - time = 12 - time_difference + if sunrise: # woz -> True time at location + woz = 12 - time_difference else: - time = 12 + time_difference + woz = 12 + time_difference - time: float = (time - time_equation) + longitude_to_hour + timezone[2] + time: float = (woz - time_equation) - longitude_to_hour + timezone[2] hour: int = int(time) minutes_left: float = time - int(time) @@ -82,3 +81,4 @@ def calc_sun_time(dt: datetime, timezone: tuple[float, float, float], sunrise: b out: datetime = datetime(year=dt.year, month=dt.month, day=dt.day, hour=hour, minute=minute, second=second) return out + diff --git a/tests/runtests.py b/tests/runtests.py index 02e9ee3..5e478e4 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -45,7 +45,7 @@ def get_testcase_results(testcase: str, expected_value: datetime.datetime = None if parser_result is None: return StatusType.PARSER_RETURNS_NONE, None - evaluator = Evaluator(parser_result, tz="Europe/Berlin", coordinates=(13.41053, 52.52437)) + evaluator = Evaluator(parser_result, tz="Europe/Berlin", coordinates=None) # Berlin (13.41053, 52.52437), Dubai (55.2962, 25.2684) try: From 7ff03cf0d6e23a89c624a390952cfc09b3190a5c Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 27 Oct 2022 18:42:05 +0200 Subject: [PATCH 31/57] Added Result object as return value, containing the time, the timezone and if used, coordinates --- datetimeparser/datetimeparser.py | 13 +++++++------ datetimeparser/utils/models.py | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 datetimeparser/utils/models.py diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index 540e83b..7b6e36b 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -2,22 +2,22 @@ Main module which provides the parse function. """ -__all__ = ['parse', '__version__', '__author__'] +__all__ = ['parse', '__version__', '__author__', 'Result'] __version__ = "0.13.5" __author__ = "aridevelopment" -import datetime from typing import Optional from datetimeparser.evaluator import Evaluator from datetimeparser.parser import Parser +from datetimeparser.utils.models import Result def parse( datetime_string: str, timezone: str = "Europe/Berlin", coordinates: Optional[tuple[float, float]] = None -) -> Optional[tuple[datetime.datetime, str, tuple[float, float]]]: +) -> Optional[Result]: """ Parses a datetime string and returns a datetime object. If the datetime string cannot be parsed, None is returned. @@ -27,16 +27,17 @@ def parse( :param coordinates: A tuple containing longitude and latitude. If coordinates are given, the timezone will be calculated, independently of the given timezone param NOTE: It takes a longer time to calculate the timezone, it can happen that it takes up to 30 seconds for a result - :return: A datetime object or None + :return: A result object containing the returned time, the timezone and optional coordinates. + If the process fails, None will be returned """ parser_result = Parser(datetime_string).parse() if parser_result is None: return None - evaluator_result, tz, coords = Evaluator(parser_result, tz=timezone, coordinates=coordinates).evaluate() + evaluator_result, tz, coordinates = Evaluator(parser_result, tz=timezone, coordinates=coordinates).evaluate() if evaluator_result is None: return None - return evaluator_result, tz, coords + return Result(evaluator_result, tz, coordinates) diff --git a/datetimeparser/utils/models.py b/datetimeparser/utils/models.py new file mode 100644 index 0000000..a666e54 --- /dev/null +++ b/datetimeparser/utils/models.py @@ -0,0 +1,18 @@ +from datetime import datetime + + +class Result: + time: datetime + timezone: str + coordinates: tuple[float, float] + + def __init__(self, time, timezone: str, coordinates: tuple[float, float] = None): + self.time = time + self.timezone = timezone + self.coordinates = coordinates + + def __repr__(self): + out: str = "'None'" + if self.coordinates: + out: str = f"[longitude='{self.coordinates[0]}', latitude='{self.coordinates[1]}]'" + return f"" From 4cf17d3b83a9f54ed95399a25dec257388b971b4 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 27 Oct 2022 18:55:11 +0200 Subject: [PATCH 32/57] Changed order in __all__ list --- datetimeparser/datetimeparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index 7b6e36b..21b147f 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -2,7 +2,7 @@ Main module which provides the parse function. """ -__all__ = ['parse', '__version__', '__author__', 'Result'] +__all__ = ['parse', 'Result', '__version__', '__author__'] __version__ = "0.13.5" __author__ = "aridevelopment" From d4c5248b1f7df360ca019743efdcf8c252ad140a Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Thu, 27 Oct 2022 18:55:56 +0200 Subject: [PATCH 33/57] Changed version number --- datetimeparser/datetimeparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index 21b147f..b139c57 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -3,7 +3,7 @@ """ __all__ = ['parse', 'Result', '__version__', '__author__'] -__version__ = "0.13.5" +__version__ = "0.14.0" __author__ = "aridevelopment" from typing import Optional From 4d1c17a94ce9b1e37a2533d09a85822bf6c77779 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Fri, 28 Oct 2022 11:56:54 +0200 Subject: [PATCH 34/57] Resolved conversation + fixed some bugs --- datetimeparser/datetimeparser.py | 4 ++-- datetimeparser/evaluator/evaluator.py | 9 +++++---- datetimeparser/evaluator/evaluatormethods.py | 14 ++++++++++---- datetimeparser/utils/models.py | 5 ++++- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index b139c57..0fb05f1 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -25,8 +25,8 @@ def parse( :param datetime_string: The datetime string to parse. :param timezone: The timezone to use. Should be a valid timezone for pytz.timezone(). Default: Europe/Berlin :param coordinates: A tuple containing longitude and latitude. If coordinates are given, the timezone will be calculated, - independently of the given timezone param - NOTE: It takes a longer time to calculate the timezone, it can happen that it takes up to 30 seconds for a result + independently of the given timezone param. + NOTE: It can take some seconds until a result is returned :return: A result object containing the returned time, the timezone and optional coordinates. If the process fails, None will be returned """ diff --git a/datetimeparser/evaluator/evaluator.py b/datetimeparser/evaluator/evaluator.py index 47c771f..1503b8b 100644 --- a/datetimeparser/evaluator/evaluator.py +++ b/datetimeparser/evaluator/evaluator.py @@ -32,8 +32,9 @@ def __init__(self, parsed_object, tz="Europe/Berlin", coordinates: Optional[tupl self.coordinates = coordinates def evaluate(self) -> Union[tuple[datetime, str, tuple[float, float]], None]: - ev_out = None - ev = EvaluatorMethods(self.parsed_object_content, self.current_datetime, self.coordinates, self.timezone.zone, self.offset) + ev_out: Optional[datetime] = None + coordinates: Optional[tuple[float, float]] = None + ev = EvaluatorMethods(self.parsed_object_content, self.current_datetime, self.timezone.zone, self.coordinates, self.offset) if self.parsed_object_type == Method.ABSOLUTE_DATE_FORMATS: ev_out = ev.evaluate_absolute_date_formats() @@ -42,7 +43,7 @@ def evaluate(self) -> Union[tuple[datetime, str, tuple[float, float]], None]: ev_out = ev.evaluate_absolute_prepositions() if self.parsed_object_type == Method.CONSTANTS: - ev_out = ev.evaluate_constants() + ev_out, coordinates = ev.evaluate_constants() if self.parsed_object_type == Method.RELATIVE_DATETIMES: ev_out = ev.evaluate_relative_datetime() @@ -54,6 +55,6 @@ def evaluate(self) -> Union[tuple[datetime, str, tuple[float, float]], None]: ev_out = ev.evaluate_datetime_delta_constants() if ev_out: - return ev_out, self.timezone.zone, self.coordinates + return ev_out, self.timezone.zone, self.coordinates or coordinates else: raise FailedEvaluation(self.parsed_object_content) diff --git a/datetimeparser/evaluator/evaluatormethods.py b/datetimeparser/evaluator/evaluatormethods.py index e3e629d..2aab74e 100644 --- a/datetimeparser/evaluator/evaluatormethods.py +++ b/datetimeparser/evaluator/evaluatormethods.py @@ -1,3 +1,5 @@ +from typing import Any, Optional + from datetimeparser.evaluator.evaluatorutils import EvaluatorUtils from datetimeparser.utils.baseclasses import * from datetimeparser.utils.enums import * @@ -11,10 +13,13 @@ class EvaluatorMethods(EvaluatorUtils): Evaluates a datetime-object from a given list returned from the parser """ - def __init__(self, parsed, current_time: datetime, coordinates: tuple[float, float], timezone: str, offset: timedelta = None): + def __init__( + self, parsed: Any, current_time: datetime, timezone: str, coordinates: Optional[tuple[float, float]], offset: timedelta = None + ): """ :param parsed: object returned from the parser :param current_time: the current datetime + :param timezone: the given timezone :param coordinates: coordinates from the timezone :param offset: the UTC-offset from the current timezone. Default: None """ @@ -105,7 +110,7 @@ def evaluate_absolute_prepositions(self) -> datetime: return base - def evaluate_constants(self) -> datetime: + def evaluate_constants(self) -> tuple[datetime, Optional[tuple[float, float]]]: dt: datetime = self.current_time object_type: Constant = self.parsed[0] @@ -132,6 +137,7 @@ def evaluate_constants(self) -> datetime: # TODO: at the moment summer and winter time change the result for the offset around 1 hour if not self.coordinates: self.coordinates = TimeZoneManager().get_coordinates(self.timezone) + dt = calc_sun_time( self.current_time, (self.coordinates[0], self.coordinates[1], ofs), @@ -149,7 +155,7 @@ def evaluate_constants(self) -> datetime: minute=dt[1], second=dt[2] ) - return dt + return dt, self.coordinates if self.current_time >= dt and self.parsed[0] not in ( Constants.ALL_RELATIVE_CONSTANTS and WeekdayConstants.ALL and DatetimeDeltaConstants.CHANGING @@ -166,7 +172,7 @@ def evaluate_constants(self) -> datetime: if object_type.offset: ev_out = self.add_relative_delta(ev_out, self.get_offset(object_type, self.offset), self.current_time) - return ev_out + return ev_out, self.coordinates def evaluate_relative_datetime(self) -> datetime: out: datetime = self.current_time diff --git a/datetimeparser/utils/models.py b/datetimeparser/utils/models.py index a666e54..e32fc24 100644 --- a/datetimeparser/utils/models.py +++ b/datetimeparser/utils/models.py @@ -6,7 +6,7 @@ class Result: timezone: str coordinates: tuple[float, float] - def __init__(self, time, timezone: str, coordinates: tuple[float, float] = None): + def __init__(self, time: datetime, timezone: str, coordinates: tuple[float, float] = None): self.time = time self.timezone = timezone self.coordinates = coordinates @@ -16,3 +16,6 @@ def __repr__(self): if self.coordinates: out: str = f"[longitude='{self.coordinates[0]}', latitude='{self.coordinates[1]}]'" return f"" + + def __str__(self): + return self.__repr__() From a97ed72213dfc63003c5a303c13f1267830b063f Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Fri, 28 Oct 2022 12:09:39 +0200 Subject: [PATCH 35/57] Adjusted README.md + added more examples --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5ffed22..7a18d62 100644 --- a/README.md +++ b/README.md @@ -30,16 +30,26 @@ Below you can find some examples of how datetimeparser can be used. from datetimeparser import parse print(parse("next 3 years and 2 months")) -# 2025-04-06 11:43:28 +# print(parse("begin of advent of code 2022")) -# 2022-12-01 06:00:00 +# print(parse("in 1 Year 2 months 3 weeks 4 days 5 hours 6 minutes 7 seconds")) -# 2023-05-01 17:59:52 +# print(parse("10 days and 2 hours after 3 months before christmas 2020")) -# 2020-10-05 02:00:00 +# + +print(parse("sunrise")) +# + +print(parse("sunrise", timezone="Asia/Dubai")) +# + +# https://www.timeanddate.com/sun/japan/tokyo states that the sunset today (2022-10-28) is at '16:50' in Tokyo +print(parse("sunset", coordinates=(139.839478, 35.652832))) # (Tokyo in Japan) +# ``` ## Installation From c159fcab0b1ec3efc985dbd3d0443afd24630305 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Fri, 28 Oct 2022 12:15:24 +0200 Subject: [PATCH 36/57] Fixed linting --- datetimeparser/evaluator/evaluatormethods.py | 2 +- datetimeparser/evaluator/evaluatorutils.py | 2 +- datetimeparser/utils/formulars.py | 1 - datetimeparser/utils/geometry.py | 4 ++-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/datetimeparser/evaluator/evaluatormethods.py b/datetimeparser/evaluator/evaluatormethods.py index 2aab74e..7ae69b4 100644 --- a/datetimeparser/evaluator/evaluatormethods.py +++ b/datetimeparser/evaluator/evaluatormethods.py @@ -133,7 +133,7 @@ def evaluate_constants(self) -> tuple[datetime, Optional[tuple[float, float]]]: ) elif object_type.name == "sunset" or object_type.name == "sunrise": - ofs = self.offset.total_seconds()/60/60 # -> to hours + ofs = self.offset.total_seconds() / 60 / 60 # -> to hours # TODO: at the moment summer and winter time change the result for the offset around 1 hour if not self.coordinates: self.coordinates = TimeZoneManager().get_coordinates(self.timezone) diff --git a/datetimeparser/evaluator/evaluatorutils.py b/datetimeparser/evaluator/evaluatorutils.py index f769fca..5a32d9d 100644 --- a/datetimeparser/evaluator/evaluatorutils.py +++ b/datetimeparser/evaluator/evaluatorutils.py @@ -109,7 +109,7 @@ def sanitize_input( current_time ) if current_time > test_out and not given_year: - parsed_list = EvaluatorUtils.x_week_of_month(relative_dt, idx, pars2, year+1) + parsed_list = EvaluatorUtils.x_week_of_month(relative_dt, idx, pars2, year + 1) return list(filter(lambda e: e not in Keywords.ALL and not isinstance(e, str), parsed_list)), given_year diff --git a/datetimeparser/utils/formulars.py b/datetimeparser/utils/formulars.py index cea869d..62dfc4a 100644 --- a/datetimeparser/utils/formulars.py +++ b/datetimeparser/utils/formulars.py @@ -81,4 +81,3 @@ def calc_sun_time(dt: datetime, timezone: tuple[float, float, float], sunrise: b out: datetime = datetime(year=dt.year, month=dt.month, day=dt.day, hour=hour, minute=minute, second=second) return out - diff --git a/datetimeparser/utils/geometry.py b/datetimeparser/utils/geometry.py index c70e284..2949e72 100644 --- a/datetimeparser/utils/geometry.py +++ b/datetimeparser/utils/geometry.py @@ -10,9 +10,9 @@ def get_coordinates(self, timezone: str) -> tuple[float, float]: coords = self.get_geometry(tz_name=timezone, coords_as_pairs=True) while not isinstance(coords[0], tuple): - coords = coords[len(coords)//2] + coords = coords[len(coords) // 2] - coords: tuple[float, float] = coords[len(coords)//2] + coords: tuple[float, float] = coords[len(coords) // 2] # timezone = self.timezone_at(lng=coords[0] + 1, lat=coords[1]) # TODO: needs to be improved, at the moment it's just a small fix, not tested if it works with all timezones From 4a2449e9173caa317b35cb8d16b213f3d19d7cea Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sat, 29 Oct 2022 12:22:04 +0200 Subject: [PATCH 37/57] Adjusted README.md + added docstring for Result class --- README.md | 16 ++++++++-------- datetimeparser/utils/models.py | 9 +++++++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7a18d62..064bec4 100644 --- a/README.md +++ b/README.md @@ -29,17 +29,17 @@ Below you can find some examples of how datetimeparser can be used. ```python from datetimeparser import parse -print(parse("next 3 years and 2 months")) -# +print(parse("next 3 years and 2 months").time) +# 2025-12-28 11:57:25 -print(parse("begin of advent of code 2022")) -# +print(parse("begin of advent of code 2022").time) +# 2022-12-01 06:00:00 -print(parse("in 1 Year 2 months 3 weeks 4 days 5 hours 6 minutes 7 seconds")) -# +print(parse("in 1 Year 2 months 3 weeks 4 days 5 hours 6 minutes 7 seconds").time) +# 2024-01-22 17:04:26 -print(parse("10 days and 2 hours after 3 months before christmas 2020")) -# +print(parse("10 days and 2 hours after 3 months before christmas 2020").time) +# 2020-10-05 02:00:00 print(parse("sunrise")) # diff --git a/datetimeparser/utils/models.py b/datetimeparser/utils/models.py index e32fc24..a0705e0 100644 --- a/datetimeparser/utils/models.py +++ b/datetimeparser/utils/models.py @@ -2,6 +2,15 @@ class Result: + """ + The returned Result by the parse function, containing the output information + + - Attributes: + - time (datetime): The parsed time + - timezone (str): The used timezone + - coordinates (Optional[tuple[float, float]]): Coordinates used for parsing + + """ time: datetime timezone: str coordinates: tuple[float, float] From 9de13354ad6983945c1431fe7515888989f1ec1b Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sat, 29 Oct 2022 12:32:14 +0200 Subject: [PATCH 38/57] Fixed wrong testcases --- tests/testcases.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testcases.py b/tests/testcases.py index df86858..1907995 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -79,8 +79,8 @@ def __new__( # Absolute prepositions "absolute_prepositions": { "second day after christmas": Expected(time_sensitive=True, month=12, day=27), - "3rd week of august": Expected(time_sensitive=True, month=8, day=22), - "4. week of august": Expected(time_sensitive=True, month=8, day=29), + "3rd week of august": Expected(time_sensitive=True, month=8, day=14), + "4. week of august": Expected(time_sensitive=True, month=8, day=21), "1st of august": Expected(time_sensitive=True, month=8, day=1), "fifth month of 2021": Expected(year=2021, month=5, day=1), "three days after the fifth of august 2018": Expected(year=2018, month=8, day=8), From 44645894d0941b1494176f38d48f8e87d702b303 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sat, 29 Oct 2022 12:57:27 +0200 Subject: [PATCH 39/57] Added fix for summer/winter-time issue --- datetimeparser/evaluator/evaluatormethods.py | 2 ++ datetimeparser/evaluator/evaluatorutils.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/datetimeparser/evaluator/evaluatormethods.py b/datetimeparser/evaluator/evaluatormethods.py index 7ae69b4..78c917c 100644 --- a/datetimeparser/evaluator/evaluatormethods.py +++ b/datetimeparser/evaluator/evaluatormethods.py @@ -171,6 +171,8 @@ def evaluate_constants(self) -> tuple[datetime, Optional[tuple[float, float]]]: if object_type.offset: ev_out = self.add_relative_delta(ev_out, self.get_offset(object_type, self.offset), self.current_time) + if self.daylight_saving(self.timezone): + ev_out -= timedelta(hours=1) return ev_out, self.coordinates diff --git a/datetimeparser/evaluator/evaluatorutils.py b/datetimeparser/evaluator/evaluatorutils.py index 5a32d9d..53f41a7 100644 --- a/datetimeparser/evaluator/evaluatorutils.py +++ b/datetimeparser/evaluator/evaluatorutils.py @@ -1,3 +1,4 @@ +import pytz from typing import Any, Tuple, Union from datetimeparser.utils.baseclasses import * @@ -252,3 +253,8 @@ def get_offset(con: Constant, offset) -> RelativeDateTime: off += con.offset return RelativeDateTime(hours=off + offset.seconds / 3600 + offset.days * 24) + + @staticmethod + def daylight_saving(tz: str): + """checks if a timezone currently saves daylight (winter-/summer-time)""" + return bool(datetime.now(pytz.timezone(tz)).dst()) From 1ee8bb8bf4a98a585dc61747868ed49f1a60cba9 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sat, 29 Oct 2022 13:07:55 +0200 Subject: [PATCH 40/57] Fixed typing --- datetimeparser/datetimeparser.py | 4 ++-- datetimeparser/evaluator/evaluator.py | 8 ++++---- datetimeparser/evaluator/evaluatormethods.py | 6 +++--- datetimeparser/utils/formulars.py | 3 ++- datetimeparser/utils/geometry.py | 7 ++++--- datetimeparser/utils/models.py | 5 +++-- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index 0fb05f1..c0574d7 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -6,7 +6,7 @@ __version__ = "0.14.0" __author__ = "aridevelopment" -from typing import Optional +from typing import Optional, Tuple from datetimeparser.evaluator import Evaluator from datetimeparser.parser import Parser @@ -16,7 +16,7 @@ def parse( datetime_string: str, timezone: str = "Europe/Berlin", - coordinates: Optional[tuple[float, float]] = None + coordinates: Optional[Tuple[float, float]] = None ) -> Optional[Result]: """ Parses a datetime string and returns a datetime object. diff --git a/datetimeparser/evaluator/evaluator.py b/datetimeparser/evaluator/evaluator.py index 1503b8b..04ab77f 100644 --- a/datetimeparser/evaluator/evaluator.py +++ b/datetimeparser/evaluator/evaluator.py @@ -1,6 +1,6 @@ from datetime import datetime from pytz import timezone, UnknownTimeZoneError -from typing import Optional, Union +from typing import Optional, Tuple, Union from datetimeparser.utils.baseclasses import AbsoluteDateTime, RelativeDateTime from datetimeparser.utils.enums import Method @@ -10,7 +10,7 @@ class Evaluator: - def __init__(self, parsed_object, tz="Europe/Berlin", coordinates: Optional[tuple[float, float]] = None): + def __init__(self, parsed_object, tz="Europe/Berlin", coordinates: Optional[Tuple[float, float]] = None): """ :param parsed_object: the parsed object from parser :param tz: the timezone for the datetime @@ -31,9 +31,9 @@ def __init__(self, parsed_object, tz="Europe/Berlin", coordinates: Optional[tupl self.timezone = tiz self.coordinates = coordinates - def evaluate(self) -> Union[tuple[datetime, str, tuple[float, float]], None]: + def evaluate(self) -> Union[Tuple[datetime, str, Tuple[float, float]], None]: ev_out: Optional[datetime] = None - coordinates: Optional[tuple[float, float]] = None + coordinates: Optional[Tuple[float, float]] = None ev = EvaluatorMethods(self.parsed_object_content, self.current_datetime, self.timezone.zone, self.coordinates, self.offset) if self.parsed_object_type == Method.ABSOLUTE_DATE_FORMATS: diff --git a/datetimeparser/evaluator/evaluatormethods.py b/datetimeparser/evaluator/evaluatormethods.py index 78c917c..15f1940 100644 --- a/datetimeparser/evaluator/evaluatormethods.py +++ b/datetimeparser/evaluator/evaluatormethods.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any, Optional, Tuple from datetimeparser.evaluator.evaluatorutils import EvaluatorUtils from datetimeparser.utils.baseclasses import * @@ -14,7 +14,7 @@ class EvaluatorMethods(EvaluatorUtils): """ def __init__( - self, parsed: Any, current_time: datetime, timezone: str, coordinates: Optional[tuple[float, float]], offset: timedelta = None + self, parsed: Any, current_time: datetime, timezone: str, coordinates: Optional[Tuple[float, float]], offset: timedelta = None ): """ :param parsed: object returned from the parser @@ -110,7 +110,7 @@ def evaluate_absolute_prepositions(self) -> datetime: return base - def evaluate_constants(self) -> tuple[datetime, Optional[tuple[float, float]]]: + def evaluate_constants(self) -> Tuple[datetime, Optional[Tuple[float, float]]]: dt: datetime = self.current_time object_type: Constant = self.parsed[0] diff --git a/datetimeparser/utils/formulars.py b/datetimeparser/utils/formulars.py index 62dfc4a..c131d65 100644 --- a/datetimeparser/utils/formulars.py +++ b/datetimeparser/utils/formulars.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta +from typing import Tuple import math @@ -45,7 +46,7 @@ def year_start(year_time: int) -> datetime: return datetime(year=year_time, month=1, day=1) -def calc_sun_time(dt: datetime, timezone: tuple[float, float, float], sunrise: bool = True) -> datetime: +def calc_sun_time(dt: datetime, timezone: Tuple[float, float, float], sunrise: bool = True) -> datetime: """ Calculates the time for sunrise and sunset based on coordinates and a date :param dt: The date for calculating the sunset diff --git a/datetimeparser/utils/geometry.py b/datetimeparser/utils/geometry.py index 2949e72..5cef2d8 100644 --- a/datetimeparser/utils/geometry.py +++ b/datetimeparser/utils/geometry.py @@ -1,4 +1,5 @@ from timezonefinder import TimezoneFinder +from typing import Tuple class TimeZoneManager(TimezoneFinder): @@ -6,13 +7,13 @@ class TimeZoneManager(TimezoneFinder): def __init__(self): super(TimeZoneManager, self).__init__(in_memory=True) - def get_coordinates(self, timezone: str) -> tuple[float, float]: + def get_coordinates(self, timezone: str) -> Tuple[float, float]: coords = self.get_geometry(tz_name=timezone, coords_as_pairs=True) - while not isinstance(coords[0], tuple): + while not isinstance(coords[0], Tuple): coords = coords[len(coords) // 2] - coords: tuple[float, float] = coords[len(coords) // 2] + coords: Tuple[float, float] = coords[len(coords) // 2] # timezone = self.timezone_at(lng=coords[0] + 1, lat=coords[1]) # TODO: needs to be improved, at the moment it's just a small fix, not tested if it works with all timezones diff --git a/datetimeparser/utils/models.py b/datetimeparser/utils/models.py index a0705e0..4fc49f7 100644 --- a/datetimeparser/utils/models.py +++ b/datetimeparser/utils/models.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import Tuple class Result: @@ -13,9 +14,9 @@ class Result: """ time: datetime timezone: str - coordinates: tuple[float, float] + coordinates: Tuple[float, float] - def __init__(self, time: datetime, timezone: str, coordinates: tuple[float, float] = None): + def __init__(self, time: datetime, timezone: str, coordinates: Tuple[float, float] = None): self.time = time self.timezone = timezone self.coordinates = coordinates From b38c340599cfaef28528f943b285c3b5a8e7d600 Mon Sep 17 00:00:00 2001 From: AlbertUnruh <73029826+AlbertUnruh@users.noreply.github.com> Date: Sat, 5 Nov 2022 10:39:49 +0100 Subject: [PATCH 41/57] fix requirements in setup.py --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 032c53d..95b4755 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,8 @@ install_requires=[ 'python-dateutil', 'pytz', - 'typing' + 'typing', + 'timezonefinder' ], classifiers=[ 'Development Status :: 4 - Beta', # Chose either "3 - Alpha", "4 - Beta" or "5 - Production/Stable" as the current state of your package From d8798ce1c632cb11459566bc829a699918ec6375 Mon Sep 17 00:00:00 2001 From: AlbertUnruh <73029826+AlbertUnruh@users.noreply.github.com> Date: Sat, 5 Nov 2022 21:44:19 +0100 Subject: [PATCH 42/57] retrieve `__version__` via regex --- setup.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 95b4755..d4b72b7 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,15 @@ +import re from distutils.core import setup from pathlib import Path -from datetimeparser.datetimeparser import __version__ - # for the readme this_directory = Path(__file__).parent +__version__ = re.search( + r"^__version__\s*=\s*[\'\"]([^\'\"]*)[\'\"]", + this_directory.joinpath("datetimeparser/datetimeparser.py").read_text(encoding="utf-8"), + re.MULTILINE +).group(1) long_description = (this_directory / "README.md").read_text(encoding="utf-8") setup( From f2408f3c7cf0cddf9a77e43cdc1ea17dc0a1f378 Mon Sep 17 00:00:00 2001 From: AlbertUnruh <73029826+AlbertUnruh@users.noreply.github.com> Date: Wed, 9 Nov 2022 17:52:58 +0100 Subject: [PATCH 43/57] remove `packages=` (setup.py) --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index d4b72b7..f245ee0 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,6 @@ name='python-datetimeparser', long_description_content_type="text/markdown", long_description=long_description, - packages=['datetimeparser'], version=".".join(__version__.split(".")[:2]) + "rc1." + __version__.split(".")[2], # version number: https://peps.python.org/pep-0440/ license='MIT', description='A parser library built for parsing the english language into datetime objects.', From 426d6c1030b2eaba8a2079186faa179651dc4825 Mon Sep 17 00:00:00 2001 From: AlbertUnruh <73029826+AlbertUnruh@users.noreply.github.com> Date: Wed, 9 Nov 2022 17:55:13 +0100 Subject: [PATCH 44/57] add `packages` (setup.cfg) --- setup.cfg | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 224a779..3f0b1ee 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,5 @@ [metadata] -description-file = README.md \ No newline at end of file +description-file = README.md + +[options] +packages = find: From 5c01aa83b43a973b52acb2c45ea34442b418cf2a Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sat, 31 Dec 2022 14:04:18 +0100 Subject: [PATCH 45/57] Removed pytz as package and replaced it with ZoneInfo from the zoneinfo module --- datetimeparser/datetimeparser.py | 2 +- datetimeparser/evaluator/evaluator.py | 12 ++++++------ datetimeparser/evaluator/evaluatorutils.py | 4 ++-- requirements.txt | 1 - tests/testcases.py | 4 ++-- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index c0574d7..fbaa0d5 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -3,7 +3,7 @@ """ __all__ = ['parse', 'Result', '__version__', '__author__'] -__version__ = "0.14.0" +__version__ = "0.15.0" __author__ = "aridevelopment" from typing import Optional, Tuple diff --git a/datetimeparser/evaluator/evaluator.py b/datetimeparser/evaluator/evaluator.py index 04ab77f..829fb85 100644 --- a/datetimeparser/evaluator/evaluator.py +++ b/datetimeparser/evaluator/evaluator.py @@ -1,6 +1,6 @@ from datetime import datetime -from pytz import timezone, UnknownTimeZoneError from typing import Optional, Tuple, Union +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from datetimeparser.utils.baseclasses import AbsoluteDateTime, RelativeDateTime from datetimeparser.utils.enums import Method @@ -20,21 +20,21 @@ def __init__(self, parsed_object, tz="Europe/Berlin", coordinates: Optional[Tupl if coordinates: tz = TimeZoneManager().timezone_at(lng=coordinates[0], lat=coordinates[1]) try: - tiz = timezone(tz) - except UnknownTimeZoneError: + tiz = ZoneInfo(tz) + except ZoneInfoNotFoundError: raise InvalidValue(f"Unknown timezone: '{tz}'") self.parsed_object_type = parsed_object[0] self.parsed_object_content: Union[list, AbsoluteDateTime, RelativeDateTime] = parsed_object[1] self.current_datetime: datetime = datetime.strptime(datetime.strftime(datetime.now(tz=tiz), "%Y-%m-%d %H:%M:%S"), "%Y-%m-%d %H:%M:%S") self.offset = tiz.utcoffset(self.current_datetime) - self.timezone = tiz + self.timezone: ZoneInfo = tiz self.coordinates = coordinates def evaluate(self) -> Union[Tuple[datetime, str, Tuple[float, float]], None]: ev_out: Optional[datetime] = None coordinates: Optional[Tuple[float, float]] = None - ev = EvaluatorMethods(self.parsed_object_content, self.current_datetime, self.timezone.zone, self.coordinates, self.offset) + ev = EvaluatorMethods(self.parsed_object_content, self.current_datetime, self.timezone.key, self.coordinates, self.offset) if self.parsed_object_type == Method.ABSOLUTE_DATE_FORMATS: ev_out = ev.evaluate_absolute_date_formats() @@ -55,6 +55,6 @@ def evaluate(self) -> Union[Tuple[datetime, str, Tuple[float, float]], None]: ev_out = ev.evaluate_datetime_delta_constants() if ev_out: - return ev_out, self.timezone.zone, self.coordinates or coordinates + return ev_out, self.timezone.key, self.coordinates or coordinates else: raise FailedEvaluation(self.parsed_object_content) diff --git a/datetimeparser/evaluator/evaluatorutils.py b/datetimeparser/evaluator/evaluatorutils.py index 53f41a7..b4f66f9 100644 --- a/datetimeparser/evaluator/evaluatorutils.py +++ b/datetimeparser/evaluator/evaluatorutils.py @@ -1,5 +1,5 @@ -import pytz from typing import Any, Tuple, Union +from zoneinfo import ZoneInfo from datetimeparser.utils.baseclasses import * from datetimeparser.utils.enums import * @@ -257,4 +257,4 @@ def get_offset(con: Constant, offset) -> RelativeDateTime: @staticmethod def daylight_saving(tz: str): """checks if a timezone currently saves daylight (winter-/summer-time)""" - return bool(datetime.now(pytz.timezone(tz)).dst()) + return bool(datetime.now(ZoneInfo(tz)).dst()) diff --git a/requirements.txt b/requirements.txt index a1e6ab3..a6f2027 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ python-dateutil -pytz typing timezonefinder diff --git a/tests/testcases.py b/tests/testcases.py index 1907995..da51713 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -1,6 +1,6 @@ from datetime import datetime from dateutil.relativedelta import relativedelta -from pytz import timezone +from zoneinfo import ZoneInfo class ThrowException: @@ -20,7 +20,7 @@ def __repr__(self): class Expected: - TODAY = datetime.strptime(datetime.now(tz=timezone("Europe/Berlin")).strftime("%Y-%m-%d %H:%M:%S"), "%Y-%m-%d %H:%M:%S") + TODAY = datetime.strptime(datetime.now(tz=ZoneInfo("Europe/Berlin")).strftime("%Y-%m-%d %H:%M:%S"), "%Y-%m-%d %H:%M:%S") def __new__( cls, now: bool = False, delta: relativedelta = None, time_sensitive: bool = False, From e828db7b04d3a503b56d2a3ddaef03c490e61794 Mon Sep 17 00:00:00 2001 From: Inf-inity <83883849+Inf-inity@users.noreply.github.com> Date: Sat, 31 Dec 2022 14:11:37 +0100 Subject: [PATCH 46/57] Added tzdata package for windows --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a6f2027..4f39626 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ python-dateutil -typing timezonefinder +typing +tzdata From 20f23bed36f2ce0dacf946f7729a75b2763b16e8 Mon Sep 17 00:00:00 2001 From: AlbertUnruh <73029826+AlbertUnruh@users.noreply.github.com> Date: Tue, 28 Mar 2023 14:51:52 +0200 Subject: [PATCH 47/57] fix version reason: `SetuptoolsDeprecationWarning: Invalid version: '0.15rc1.0'` during installation via `python-datetimeparser @ git+https://github.com/aridevelopment-de/datetimeparser.git@dev` --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f245ee0..fa3bd79 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ name='python-datetimeparser', long_description_content_type="text/markdown", long_description=long_description, - version=".".join(__version__.split(".")[:2]) + "rc1." + __version__.split(".")[2], # version number: https://peps.python.org/pep-0440/ + version=__version__, license='MIT', description='A parser library built for parsing the english language into datetime objects.', author='Ari24', From d95e85aec205dafc25376022cd9be0f7dffe158a Mon Sep 17 00:00:00 2001 From: Ari van Houten Date: Tue, 28 Mar 2023 15:27:05 +0200 Subject: [PATCH 48/57] Fixed some testcases not taking current year into account --- tests/testcases.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/testcases.py b/tests/testcases.py index da51713..b99a87c 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -64,6 +64,10 @@ def __new__( return excepted_time +def is_leap_year(year: int) -> bool: + return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) + + testcases = { # Absolute datetime formats "absolute_datetime_formats": { @@ -79,8 +83,8 @@ def __new__( # Absolute prepositions "absolute_prepositions": { "second day after christmas": Expected(time_sensitive=True, month=12, day=27), - "3rd week of august": Expected(time_sensitive=True, month=8, day=14), - "4. week of august": Expected(time_sensitive=True, month=8, day=21), + "3rd week of august": None, # Removed, because 3rd week of August is different for each year + "4. week of august": None, # Same reasoning as above "1st of august": Expected(time_sensitive=True, month=8, day=1), "fifth month of 2021": Expected(year=2021, month=5, day=1), "three days after the fifth of august 2018": Expected(year=2018, month=8, day=8), @@ -141,7 +145,7 @@ def __new__( "eastern 2010": Expected(year=2010, month=4, day=4), "halloween 2030": Expected(year=2030, month=10, day=31), "next april fools day": Expected(time_sensitive=True, month=4, day=1), - "thanksgiving": Expected(time_sensitive=True, month=11, day=24), + "thanksgiving": Expected(time_sensitive=True, month=11, day=23), "next st patricks day": Expected(time_sensitive=True, month=3, day=17), "valentine day 2010": Expected(year=2010, month=2, day=14), "summer": Expected(time_sensitive=True, month=6, day=1), @@ -149,7 +153,8 @@ def __new__( "next spring": Expected(time_sensitive=True, month=3, day=1), "begin of fall 2010": Expected(year=2010, month=9, day=1), "summer end": Expected(time_sensitive=True, month=8, day=31, hour=23, minute=59, second=59), - "end of winter": Expected(time_sensitive=True, month=2, day=28, hour=23, minute=59, second=59), + f"end of winter {datetime.today().year}": Expected(year=datetime.today().year, month=2, day=28 + is_leap_year(datetime.today().year), + hour=23, minute=59, second=59), "end of the spring": Expected(time_sensitive=True, month=5, day=31, hour=23, minute=59, second=59), "end of autumn 2020": Expected(year=2020, month=11, day=30, hour=23, minute=59, second=59), "begin of advent of code 2022": Expected(year=2022, month=12, day=1, hour=6), From e69c87b7286506488cdfc931fa9d91043791080d Mon Sep 17 00:00:00 2001 From: Ari van Houten Date: Tue, 28 Mar 2023 16:02:10 +0200 Subject: [PATCH 49/57] 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 50/57] 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 51/57] 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 52/57] 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 53/57] 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 54/57] 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 55/57] 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: From 213f9a01e4fd703ca3561f23bcf7aec0de1424ad Mon Sep 17 00:00:00 2001 From: Ari van Houten Date: Wed, 5 Apr 2023 16:49:00 +0200 Subject: [PATCH 56/57] Added 3.11 to GitHub workflow test matrix --- .github/workflows/python-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 522ac6a..bcb3f7a 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v2 From 552e17cba9c29db95bcc81f891b05b87ef2ca884 Mon Sep 17 00:00:00 2001 From: Ari van Houten Date: Wed, 5 Apr 2023 16:52:23 +0200 Subject: [PATCH 57/57] Bumped version to 1.0.0 --- datetimeparser/datetimeparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datetimeparser/datetimeparser.py b/datetimeparser/datetimeparser.py index fbaa0d5..d6e5df8 100644 --- a/datetimeparser/datetimeparser.py +++ b/datetimeparser/datetimeparser.py @@ -3,7 +3,7 @@ """ __all__ = ['parse', 'Result', '__version__', '__author__'] -__version__ = "0.15.0" +__version__ = "1.0.0" __author__ = "aridevelopment" from typing import Optional, Tuple