From 3123be0cbce18c76870f8ddcd7cc9d5effcd2f1c Mon Sep 17 00:00:00 2001 From: Thomas Zeutschler Date: Sun, 22 Sep 2024 20:05:57 +0200 Subject: [PATCH] v0.2 --- datespan/__init__.py | 4 +- datespan/date_span.py | 67 ++++++++++++++---------------- datespan/date_span_set.py | 56 ++++++++++++------------- datespan/parser/__init__.py | 2 +- datespan/parser/datespanparser.py | 6 ++- datespan/parser/errors.py | 4 ++ datespan/parser/evaluator.py | 51 +++++++++++------------ datespan/parser/lexer.py | 38 ++++++++--------- datespan/parser/parser.py | 35 +++++++++------- pyproject.toml | 2 +- tests/__init__.py | 0 tests/test_class_DateSpan.py | 7 +++- tests/test_class_DateSpanParser.py | 24 +++++------ tests/test_class_DateSpanSet.py | 4 +- tests/test_datespan_basics.py | 9 ++-- tests/test_datespan_methods.py | 7 +++- tests/test_datespanset.py | 6 +-- tests/test_debugging.py | 4 +- 18 files changed, 169 insertions(+), 157 deletions(-) create mode 100644 tests/__init__.py diff --git a/datespan/__init__.py b/datespan/__init__.py index 4da8ff6..da78907 100644 --- a/datespan/__init__.py +++ b/datespan/__init__.py @@ -1,6 +1,7 @@ # datespan - Copyright (c)2024, Thomas Zeutschler, MIT license from __future__ import annotations + from dateutil.parser import parserinfo from datespan.date_span import DateSpan @@ -11,7 +12,6 @@ __license__ = "MIT" VERSION = __version__ - __all__ = [ "DateSpanSet", "DateSpan", @@ -20,7 +20,7 @@ ] -def parse(datespan_text: str, parser_info: parserinfo | None = None) -> DateSpanSet: +def parse(datespan_text: str, parser_info: parserinfo | None = None) -> DateSpanSet: """ Creates a new DateSpanSet instance and parses the given text into a set of DateSpan objects. diff --git a/datespan/date_span.py b/datespan/date_span.py index 8235b1c..f3d7da5 100644 --- a/datespan/date_span.py +++ b/datespan/date_span.py @@ -1,9 +1,12 @@ # datespan - Copyright (c)2024, Thomas Zeutschler, MIT license from __future__ import annotations + from datetime import datetime, time, timedelta -from dateutil.relativedelta import relativedelta + from dateutil.relativedelta import MO +from dateutil.relativedelta import relativedelta + class DateSpan: """ @@ -19,7 +22,7 @@ class DateSpan: MAX_YEAR = datetime.max.year """The maximum year that can be represented by the DateSpan.""" - def __init__(self, start = None, end = None, message: str | None = None): + def __init__(self, start=None, end=None, message: str | None = None): """ Initializes a new DateSpan with the given start and end date. If only one date is given, the DateSpan will represent a single point in time. If no date is given, the DateSpan will be undefined. @@ -127,7 +130,6 @@ def almost_equals(self, other: DateSpan, epsilon: int = TIME_EPSILON_MICROSECOND return min <= start_diff <= max and min <= end_diff <= max - def merge(self, other: DateSpan) -> DateSpan: """ Returns a new DateSpan that is the merge of the DateSpan with the given DateSpan. Merging is only @@ -149,9 +151,6 @@ def can_merge(self, other: DateSpan) -> bool: return True return self.overlaps_with(other) or self.consecutive_with(other) - - - def intersect(self, other: DateSpan) -> DateSpan: """ Returns a new DateSpan that is the intersection of the DateSpan with the given DateSpan. @@ -301,7 +300,7 @@ def full_day(self) -> DateSpan: """ if self.is_undefined: return DateSpan(datetime.now().replace(hour=0, minute=0, second=0, microsecond=0), - datetime.now().replace(hour=23, minute=59, second=59, microsecond=999999)) + datetime.now().replace(hour=23, minute=59, second=59, microsecond=999999)) return DateSpan(self._start.replace(hour=0, minute=0, second=0, microsecond=0), self._end.replace(hour=23, minute=59, second=59, microsecond=999999)) @@ -350,7 +349,6 @@ def full_year(self) -> DateSpan: return DateSpan(start.replace(hour=0, minute=0, second=0, microsecond=0), end.replace(hour=23, minute=59, second=59, microsecond=999999)) - @property def ltm(self) -> DateSpan: """ @@ -371,8 +369,8 @@ def ytd(self) -> DateSpan: """ if self.is_undefined: return DateSpan.today().with_start(DateSpan.today().full_year.start) - return DateSpan(start = self.with_start(self.full_year.start).start, - end = self.end.replace(hour=23, minute=59, second=59, microsecond=999999)) + return DateSpan(start=self.with_start(self.full_year.start).start, + end=self.end.replace(hour=23, minute=59, second=59, microsecond=999999)) @property def mtd(self) -> DateSpan: @@ -381,9 +379,8 @@ def mtd(self) -> DateSpan: """ if self.is_undefined: return DateSpan.today().with_start(DateSpan.today().full_month.start) - return DateSpan(start = self.with_start(self.full_month.start).start, - end = self.end.replace(hour=23, minute=59, second=59, microsecond=999999)) - + return DateSpan(start=self.with_start(self.full_month.start).start, + end=self.end.replace(hour=23, minute=59, second=59, microsecond=999999)) @property def qtd(self) -> DateSpan: @@ -392,9 +389,8 @@ def qtd(self) -> DateSpan: """ if self.is_undefined: return DateSpan.today().with_start(DateSpan.today().full_quarter.start) - return DateSpan(start = self.with_start(self.full_quarter.start).start, - end = self.end.replace(hour=23, minute=59, second=59, microsecond=999999)) - + return DateSpan(start=self.with_start(self.full_quarter.start).start, + end=self.end.replace(hour=23, minute=59, second=59, microsecond=999999)) @property def wtd(self) -> DateSpan: @@ -403,8 +399,8 @@ def wtd(self) -> DateSpan: """ if self.is_undefined: return DateSpan.today().with_start(DateSpan.today().full_week.start) - return DateSpan(start = self.with_start(self.full_week.start).start, - end = self.end.replace(hour=23, minute=59, second=59, microsecond=999999)) + return DateSpan(start=self.with_start(self.full_week.start).start, + end=self.end.replace(hour=23, minute=59, second=59, microsecond=999999)) def _begin_of_day(self, dt: datetime) -> datetime: """Returns the beginning of the day for the given datetime.""" @@ -697,7 +693,6 @@ def to_tuple_list(self) -> list[tuple[datetime, datetime]]: """ return [(self._start, self._end), ] - # region Static Days, Month and other calculations @classmethod def max(cls) -> DateSpan: @@ -733,12 +728,13 @@ def undefined(cls) -> DateSpan: return DateSpan(None, None) @classmethod - def _monday(cls, base: datetime | None = None, offset_weeks: int = 0, offset_years: int = 0, offset_months: int = 0, offset_days: int = 0) -> DateSpan: + def _monday(cls, base: datetime | None = None, offset_weeks: int = 0, offset_years: int = 0, offset_months: int = 0, + offset_days: int = 0) -> DateSpan: # Monday is 0 and Sunday is 6 - if base is None: + if base is None: base = datetime.now() dtv = base + relativedelta(weekday=MO(-1), years=offset_years, - months=offset_months, days=offset_days, weeks=offset_weeks) + months=offset_months, days=offset_days, weeks=offset_weeks) return DateSpan(dtv).full_day @property @@ -747,7 +743,7 @@ def monday(self): Returns the Monday relative to the week of the start date time of the DateSpan. If the DateSpan is undefined, the current week's Monday will be returned. """ - return self._monday(base = self._start) + return self._monday(base=self._start) @property def tuesday(self): @@ -755,7 +751,7 @@ def tuesday(self): Returns the Tuesday relative to the week of the start date time of the DateSpan. If the DateSpan is undefined, the current week's Tuesday will be returned. """ - return self._monday(base = self._start).shift(days=1) + return self._monday(base=self._start).shift(days=1) @property def wednesday(self): @@ -763,7 +759,7 @@ def wednesday(self): Returns the Wednesday relative to the week of the start date time of the DateSpan. If the DateSpan is undefined, the current week's Wednesday will be returned. """ - return self._monday(base = self._start).shift(days=2) + return self._monday(base=self._start).shift(days=2) @property def thursday(self): @@ -771,7 +767,7 @@ def thursday(self): Returns the Thursday relative to the week of the start date time of the DateSpan. If the DateSpan is undefined, the current week's Thursday will be returned. """ - return self._monday(base = self._start).shift(days=3) + return self._monday(base=self._start).shift(days=3) @property def friday(self): @@ -779,7 +775,7 @@ def friday(self): Returns the Friday relative to the week of the start date time of the DateSpan. If the DateSpan is undefined, the current week's Friday will be returned. """ - return self._monday(base = self._start).shift(days=4) + return self._monday(base=self._start).shift(days=4) @property def saturday(self): @@ -787,7 +783,7 @@ def saturday(self): Returns the Saturday relative to the week of the start date time of the DateSpan. If the DateSpan is undefined, the current week's Saturday will be returned. """ - return self._monday(base = self._start).shift(days=5) + return self._monday(base=self._start).shift(days=5) @property def sunday(self): @@ -795,8 +791,7 @@ def sunday(self): Returns the Sunday relative to the week of the start date time of the DateSpan. If the DateSpan is undefined, the current week's Sunday will be returned. """ - return self._monday(base = self._start).shift(days=6) - + return self._monday(base=self._start).shift(days=6) @property def january(self): @@ -917,6 +912,7 @@ def december(self): if self.is_undefined: return DateSpan.now().replace(month=12).full_month return self._start.replace(month=12).full_month + # endregion # region magic methods @@ -959,7 +955,7 @@ def __str__(self): start = f"'{self._arg_start}'" if isinstance(self._arg_start, str) else str(self._arg_start) end = f"'{self._arg_end}'" if isinstance(self._arg_end, str) else str(self._arg_end) - return f"DateSpan({start}, {end})" # -> ('start': {self._start}, 'end': {self._end})" + return f"DateSpan({start}, {end})" # -> ('start': {self._start}, 'end': {self._end})" def __repr__(self): return self.__str__() @@ -1038,6 +1034,7 @@ def __le__(self, other): def __hash__(self): return hash((self._start, self._end)) + # endregion # region private methods @@ -1052,18 +1049,18 @@ def _swap(self) -> DateSpan: self._end = tmp return self - def _parse(self, start, end = None) -> (datetime, datetime): + def _parse(self, start, end=None) -> (datetime, datetime): """Parse a date span string.""" if end is None: expected_spans = 1 text = start else: expected_spans = 2 - text = f"{start}; {end}" # merge start and end into a single date span statement + text = f"{start}; {end}" # merge start and end into a single date span statement self._message = None try: - from datespan.parser.datespanparser import DateSpanParser # overcome circular import + from datespan.parser.datespanparser import DateSpanParser # overcome circular import date_span_parser: DateSpanParser = DateSpanParser(text) expressions = date_span_parser.parse() # todo: inject self.parser_info if len(expressions) != expected_spans: @@ -1080,4 +1077,4 @@ def _parse(self, start, end = None) -> (datetime, datetime): return start, end except Exception as e: self._message = str(e) - raise ValueError(str(e)) \ No newline at end of file + raise ValueError(str(e)) diff --git a/datespan/date_span_set.py b/datespan/date_span_set.py index e24cdf3..91d41e3 100644 --- a/datespan/date_span_set.py +++ b/datespan/date_span_set.py @@ -1,13 +1,15 @@ # datespan - Copyright (c)2024, Thomas Zeutschler, MIT license from __future__ import annotations -from typing import Any + import uuid from datetime import datetime, date, time +from typing import Any + from dateutil.parser import parserinfo -from datespan.parser.datespanparser import DateSpanParser from datespan.date_span import DateSpan +from datespan.parser.datespanparser import DateSpanParser class DateSpanSet: @@ -16,6 +18,7 @@ class DateSpanSet: Provides methods to filter, merge, subtract and compare DateSpan objects as well as to convert them into SQL fragments or filter functions for Python, Pandas or others. """ + def __init__(self, definition: Any | None = None, parser_info: parserinfo | None = None): """ Initializes a new DateSpanSet based on a given set of date span set definition. @@ -52,7 +55,7 @@ def __init__(self, definition: Any | None = None, parser_info: parserinfo | None elif isinstance(item, DateSpanSet): definitions.append(str(item._definition)) expressions.extend(item._spans) - elif isinstance(item, datetime | time | date): + elif isinstance(item, datetime | time | date): definitions.append(str(item)) expressions.append(item) elif isinstance(item, str): @@ -155,7 +158,7 @@ def __contains__(self, item) -> bool: elif isinstance(item, DateSpanSet): test_spans.extend(item._spans) else: - return False # unsupported type + return False # unsupported type # todo: implement more efficient algorithm, check for start and end dates for span in self._spans: @@ -172,6 +175,7 @@ def __hash__(self) -> int: def __copy__(self) -> DateSpanSet: return self.clone() + # endregion @property @@ -201,13 +205,13 @@ def clone(self) -> DateSpanSet: dss._parser_info = self._parser_info return dss - def add(self, other:DateSpanSet | DateSpan | str): + def add(self, other: DateSpanSet | DateSpan | str): """ Adds a new DateSpan object to the DateSpanSet.""" merged = self.merge(other) self._spans = merged._spans self._definition = merged._definition - def remove(self, other:DateSpanSet | DateSpan | str): + def remove(self, other: DateSpanSet | DateSpan | str): """ Removes a DateSpan object from the DateSpanSet.""" self._spans = self.intersect(other)._spans @@ -220,11 +224,11 @@ def shift(self, years: int = 0, months: int = 0, days: int = 0, hours: int = 0, new_spans: list[DateSpan] = [] for span in self._spans: new_spans.append(span.shift(years=years, months=months, days=days, hours=hours, minutes=minutes, - seconds=seconds, microseconds=microseconds, weeks=weeks)) - return DateSpanSet(new_spans) + seconds=seconds, microseconds=microseconds, weeks=weeks)) + return DateSpanSet(new_spans) raise ValueError("Failed to shift empty DateSpanSet.") - # endregion + # endregion # region Class Methods @classmethod @@ -245,7 +249,7 @@ def parse(cls, datespan_text: str, parser_info: parserinfo | None = None) -> Dat >>> DateSpanSet.parse('last month') # if today would be in February 2024 DateSpanSet([DateSpan(datetime.datetime(2024, 1, 1, 0, 0), datetime.datetime(2024, 1, 31, 23, 59, 59, 999999))]) """ - return cls(definition=datespan_text,parser_info=parser_info) + return cls(definition=datespan_text, parser_info=parser_info) @classmethod def try_parse(cls, datespan_text: str, parser_info: parserinfo | None = None) -> DateSpanSet | None: @@ -270,11 +274,12 @@ def try_parse(cls, datespan_text: str, parser_info: parserinfo | None = None) -> return dss except ValueError: return None + # endregion - # region Data Processing Methods And Callables - def to_sql(self, column: str, line_breaks: bool = False, add_comment: bool = True, indentation_in_tabs:int = 0) -> str: + def to_sql(self, column: str, line_breaks: bool = False, add_comment: bool = True, + indentation_in_tabs: int = 0) -> str: """ Converts the date spans representing the DateFilter into an ANSI-SQL compliant SQL fragment to be used for the execution of SQL queries. @@ -309,7 +314,6 @@ def to_sql(self, column: str, line_breaks: bool = False, add_comment: bool = Tru return "OR\n".join(filters) return " OR ".join(filters) + inline_comment - def to_function(self, return_sourceCde: bool = False) -> callable | str: """ Generate a compiled Python function that can be directly used as a filter function @@ -438,7 +442,8 @@ def to_tuples(self) -> list[tuple[datetime, datetime]]: """ Returns a list of tuples with start and end dates of all DateSpan objects in the DateSpanSet.""" return [(ds.start, ds.end) for ds in self._spans] - def filter(self, data: Any, column:str | None = None, return_mask:bool = False, return_index:bool=False) -> Any: + def filter(self, data: Any, column: str | None = None, return_mask: bool = False, + return_index: bool = False) -> Any: """ Filters the given data object, e.g. a Pandas DataFrame or Series, based on the date spans of the DateSpanSet. @@ -470,7 +475,7 @@ def filter(self, data: Any, column:str | None = None, return_mask:bool = False, case "pandas.core.frame.DataFrame": if column is None: raise ValueError("A column name must be provided to filter a Pandas DataFrame.") - mask = self.to_df_lambda()(data[column]) + mask = self.to_df_lambda()(data[column]) if return_mask: return mask elif return_index: @@ -478,7 +483,7 @@ def filter(self, data: Any, column:str | None = None, return_mask:bool = False, return data[mask] case "pandas.core.series.Series": - mask = self.to_df_lambda()(data) + mask = self.to_df_lambda()(data) if return_mask: return mask elif return_index: @@ -486,12 +491,11 @@ def filter(self, data: Any, column:str | None = None, return_mask:bool = False, return data[mask] case _: raise ValueError(f"Objects of type '{class_name}' are not yet supported for filtering.") - # endregion - + # endregion # region Set Operations - def merge(self, other:DateSpanSet | DateSpan | str) -> DateSpanSet: + def merge(self, other: DateSpanSet | DateSpan | str) -> DateSpanSet: """ Merges the current DateSpanSet with another DateSpanSet, DateSpan or a string representing a data span. The resulting DateSpanSet will contain date spans representing all data spans of the current and the other @@ -511,8 +515,7 @@ def merge(self, other:DateSpanSet | DateSpan | str) -> DateSpanSet: return DateSpanSet([self, DateSpanSet(other)]) raise ValueError(f"Objects of type '{type(other)}' are not supported for DateSpanSet merging.") - - def intersect(self, other:DateSpanSet | DateSpan | str) -> DateSpanSet: + def intersect(self, other: DateSpanSet | DateSpan | str) -> DateSpanSet: """ Intersects the current DateSpanSet with another DateSpanSet, DateSpan or a string representing a data span. The resulting DateSpanSet will contain data spans that represent the current DataSpanSet minus the date spans @@ -526,7 +529,7 @@ def intersect(self, other:DateSpanSet | DateSpan | str) -> DateSpanSet: """ raise NotImplementedError() - def subtract(self, other:DateSpanSet | DateSpan | str) -> DateSpanSet: + def subtract(self, other: DateSpanSet | DateSpan | str) -> DateSpanSet: """ Subtracts a DateSpanSet, DateSpan or a string representing a data span from the current DateSpanSet. So, the resulting DateSpanSet will contain data spans that represent the current DataSpanSet minus @@ -574,22 +577,19 @@ def subtract(self, other:DateSpanSet | DateSpan | str) -> DateSpanSet: dss._definition = " - ".join(definitions) return dss - # end region - - # region Internal Methods def _merge_all(self): """ Merges all overlapping DateSpan objects if applicable. """ if len(self._spans) < 2: - return # special case, just one span = nothing to merge + return # special case, just one span = nothing to merge self._spans.sort() - current:DateSpan = self._spans[0] + current: DateSpan = self._spans[0] stack = self._spans[1:] stack.reverse() merged: list[DateSpan] = [] @@ -615,7 +615,7 @@ def _parse(self, text: str | None = None): self._spans.clear() try: date_span_parser: DateSpanParser = DateSpanParser(text) - expressions = date_span_parser.parse() # todo: inject self.parser_info + expressions = date_span_parser.parse() # todo: inject self.parser_info for expr in expressions: self._spans.extend([DateSpan(span[0], span[1]) for span in expr]) except Exception as e: diff --git a/datespan/parser/__init__.py b/datespan/parser/__init__.py index 42ae13f..a7ad799 100644 --- a/datespan/parser/__init__.py +++ b/datespan/parser/__init__.py @@ -3,4 +3,4 @@ MIN_YEAR = 1700 """The minimum year that can be represented by the DateSpan.""" MAX_YEAR = 2300 -"""The maximum year that can be represented by the DateSpan.""" \ No newline at end of file +"""The maximum year that can be represented by the DateSpan.""" diff --git a/datespan/parser/datespanparser.py b/datespan/parser/datespanparser.py index fe70027..5369621 100644 --- a/datespan/parser/datespanparser.py +++ b/datespan/parser/datespanparser.py @@ -1,15 +1,17 @@ # datespan - Copyright (c)2024, Thomas Zeutschler, MIT license -from datespan.parser.errors import ParsingError, EvaluationError +from datespan.parser.errors import ParsingError from datespan.parser.evaluator import Evaluator from datespan.parser.lexer import Lexer from datespan.parser.parser import Parser + class DateSpanParser: """ The DateSpanParser class serves as the main interface. It takes an input string, tokenizes it, parses the tokens into an AST, and evaluates the AST to produce date spans. """ + def __init__(self, text): self.text = str(text).strip() self.lexer = None @@ -56,4 +58,4 @@ def date_spans(self): """ Returns the evaluated date spans from the evaluator. """ - return self.evaluator.evaluated_spans if self.evaluator else [] \ No newline at end of file + return self.evaluator.evaluated_spans if self.evaluator else [] diff --git a/datespan/parser/errors.py b/datespan/parser/errors.py index 83f469f..bdcc59c 100644 --- a/datespan/parser/errors.py +++ b/datespan/parser/errors.py @@ -4,6 +4,7 @@ class ParsingError(Exception): """ Exception raised when a parsing error occurs, including position and token information. """ + def __init__(self, message, line=0, column=0, token_value=None): super().__init__(message) self.line = line @@ -12,6 +13,7 @@ def __init__(self, message, line=0, column=0, token_value=None): def __str__(self): return f"{super().__str__()} at line: {self.line}, column: {self.column}, token: '{self.token_value}'." + def __repr__(self): return self.__str__() @@ -20,6 +22,7 @@ class EvaluationError(Exception): """ Exception raised when an evaluation error occurs. """ + def __init__(self, message, line=0, column=0, token_value=None): super().__init__(message) self.line = line @@ -28,5 +31,6 @@ def __init__(self, message, line=0, column=0, token_value=None): def __str__(self): return f"{super().__str__()} at line: {self.line}, column: {self.column}, token: '{self.token_value}'." + def __repr__(self): return self.__str__() diff --git a/datespan/parser/evaluator.py b/datespan/parser/evaluator.py index 4654acd..46bed51 100644 --- a/datespan/parser/evaluator.py +++ b/datespan/parser/evaluator.py @@ -6,11 +6,11 @@ import dateutil.parser from dateutil.relativedelta import relativedelta +from datespan.date_span import DateSpan from datespan.parser import MIN_YEAR, MAX_YEAR from datespan.parser.errors import EvaluationError, ParsingError from datespan.parser.lexer import Token, TokenType, Lexer from datespan.parser.parser import Parser -from datespan.date_span import DateSpan class Evaluator: @@ -18,6 +18,7 @@ class Evaluator: The Evaluator class takes the AST produced by the parser_old and computes the actual date spans. It handles the logic of converting relative dates and special keywords into concrete date ranges. """ + def __init__(self, statements): self.statements = statements # List of statements (AST nodes) self.today = datetime.today() # Current date and time @@ -106,7 +107,6 @@ def evaluate_specific_date(self, date_str): start = datetime.combine(date.date(), date.time()) end = start + timedelta(seconds=1, microseconds=-1) - return [(start, end)] def evaluate_range(self, start_tokens, end_tokens): @@ -190,7 +190,8 @@ def evaluate_half_bound(self, tokens, keyword): try: ast_nodes = parser.parse_statement() except ParsingError as e: - raise EvaluationError(f"Failed to parse date of date span after 'since', 'after', 'before', or 'until': {e}") + raise EvaluationError( + f"Failed to parse date of date span after 'since', 'after', 'before', or 'until': {e}") if not ast_nodes: raise EvaluationError(f"Date of date span missing after " f"'since', 'after', 'before', or 'until'.") @@ -204,7 +205,7 @@ def evaluate_half_bound(self, tokens, keyword): raise EvaluationError(f"Failed to evaluate date of date span after " f"'since', 'after', 'before', or 'until'.") - if keyword == 'since': + if keyword == 'since': start_date = spans[0][0] end_date = self.today elif keyword == 'from': @@ -222,8 +223,6 @@ def evaluate_half_bound(self, tokens, keyword): f"'after', 'before', or 'until' but got '{keyword}'.") return [(start_date, end_date)] - - def evaluate_iterative(self, tokens, period_tokens): """ Evaluates an iterative date expression and returns the corresponding date spans. @@ -357,10 +356,9 @@ def evaluate_relative(self, tokens): return self.evaluate_special(token.value) idx += 1 - - if direction == 'previous': # incl. 'last' + if direction == 'previous': # incl. 'last' return self.calculate_previous(number, unit) - if direction == 'rolling': # incl. 'past' + if direction == 'rolling': # incl. 'past' return self.calculate_rolling(number, unit) if direction == 'next': return self.calculate_future(number, unit) @@ -370,7 +368,7 @@ def evaluate_relative(self, tokens): if ordinal is not None: # Handle expressions like '1st Monday' return self.calculate_nth_weekday_in_period(ordinal, unit) - else: # direction = None + else: # direction = None if unit in ['day', 'week', 'month', 'year', 'quarter']: return self.calculate_this(unit) return [] @@ -388,38 +386,38 @@ def evaluate_special(self, value, date_spans: list = None): elif value == 'now': return DateSpan.now().to_tuple_list() - elif value == 'ltm': # last 12 month + elif value == 'ltm': # last 12 month if date_spans: date_spans.sort() - base = date_spans[-1][1] # latest end date + base = date_spans[-1][1] # latest end date span = DateSpan(base).shift_start(years=-1) return span.to_tuple_list() return DateSpan().ltm.to_tuple_list() elif value == 'ytd': if date_spans: date_spans.sort() - base = date_spans[-1][1] # latest end date + base = date_spans[-1][1] # latest end date span = DateSpan(DateSpan(base).full_year.start, base) return span.to_tuple_list() return DateSpan().ytd.to_tuple_list() elif value == 'qtd': if date_spans: date_spans.sort() - base = date_spans[-1][1] # latest end date + base = date_spans[-1][1] # latest end date span = DateSpan(DateSpan(base).full_quarter.start, base) return span.to_tuple_list() return DateSpan().qtd.to_tuple_list() elif value == 'mtd': if date_spans: date_spans.sort() - base = date_spans[-1][1] # latest end date + base = date_spans[-1][1] # latest end date span = DateSpan(DateSpan(base).full_month.start, base) return span.to_tuple_list() return DateSpan().mtd.to_tuple_list() elif value == 'wtd': if date_spans: date_spans.sort() - base = date_spans[-1][1] # latest end date + base = date_spans[-1][1] # latest end date span = DateSpan(DateSpan(base).full_week.start, base) return span.to_tuple_list() return DateSpan().wtd.to_tuple_list() @@ -448,7 +446,7 @@ def evaluate_special(self, value, date_spans: list = None): year = DateSpan.now().start.year quarter = int(value[1]) month = 3 * (quarter - 1) + 1 - return DateSpan(datetime(year= year, month=month, day=1)).full_quarter.to_tuple_list() + return DateSpan(datetime(year=year, month=month, day=1)).full_quarter.to_tuple_list() elif value == 'py': return DateSpan.now().shift(years=-1).full_year.to_tuple_list() elif value == 'cy': @@ -460,7 +458,7 @@ def evaluate_special(self, value, date_spans: list = None): else: return [] - def evaluate_triplet(self, triplet:str): + def evaluate_triplet(self, triplet: str): if not ((triplet[0] in ['r', 'p', 'l', 'n']) and (triplet[-1] in ['d', 'w', 'm', 'q', 'y']) and @@ -474,7 +472,7 @@ def evaluate_triplet(self, triplet:str): unit = unit_map[unit_char] if relative in ['r']: return self.calculate_rolling(number, unit) - elif relative in ['l', 'p']: + elif relative in ['l', 'p']: return self.calculate_previous(number, unit) elif relative == 'n': return self.calculate_future(number, unit) @@ -506,7 +504,6 @@ def evaluate_months(self, tokens): # check if the last token is a special like 'ytd' tokens, special_token = self._extract_special_token(tokens) - # Check if the last token is a number (year) if tokens and tokens[-1].type == TokenType.NUMBER: year = tokens[-1].value @@ -575,7 +572,7 @@ def calculate_rolling(self, number, unit): 'rolling 3 months': Refers to a rolling 3-month window, starting from today’s date. Note: Rolling and past are synonyms. """ - if unit == 'month': # most used units first + if unit == 'month': # most used units first return DateSpan.now().shift_start(months=-number).to_tuple_list() elif unit == 'year': return DateSpan.now().shift_start(years=-number).to_tuple_list() @@ -602,7 +599,7 @@ def calculate_previous(self, number, unit): 'previous 3 months': Refers to the full 3 calendar months immediately before the current month. Note: Previous and last are synonyms. """ - if unit == 'month': # most used units first + if unit == 'month': # most used units first return DateSpan.today().shift(months=-1).full_month.shift_start(months=-(number - 1)).to_tuple_list() elif unit == 'year': return DateSpan.today().shift(years=-1).full_year.shift_start(years=-(number - 1)).to_tuple_list() @@ -619,11 +616,11 @@ def calculate_previous(self, number, unit): elif unit == 'second': return DateSpan.now().shift(seconds=-1).full_second.shift_start(seconds=-(number - 1)).to_tuple_list() elif unit == 'millisecond': - return DateSpan.now().shift(microseconds=-1000).full_millisecond.shift_start(microseconds=-(number - 1) * 1000).to_tuple_list() + return DateSpan.now().shift(microseconds=-1000).full_millisecond.shift_start( + microseconds=-(number - 1) * 1000).to_tuple_list() else: return [] - def calculate_future(self, number, unit): """ Calculates a future date range based on the specified number and unit. @@ -637,7 +634,7 @@ def calculate_future(self, number, unit): elif unit == 'year': return DateSpan.today().shift(years=1).full_year.shift_end(years=(number - 1)).to_tuple_list() elif unit == 'quarter': - return DateSpan.today().shift(months=3).full_quarter.shift_end(months=(number - 1)*3).to_tuple_list() + return DateSpan.today().shift(months=3).full_quarter.shift_end(months=(number - 1) * 3).to_tuple_list() elif unit == 'hour': return DateSpan.now().shift(hours=1).full_hour.shift_end(hours=number - 1).to_tuple_list() elif unit == 'minute': @@ -645,11 +642,11 @@ def calculate_future(self, number, unit): elif unit == 'second': return DateSpan.now().shift(seconds=1).full_second.shift_end(seconds=number - 1).to_tuple_list() elif unit == 'millisecond': - return DateSpan.now().shift(microseconds=1000).full_millisecond.shift_end(microseconds=(number - 1) * 1000).to_tuple_list() + return DateSpan.now().shift(microseconds=1000).full_millisecond.shift_end( + microseconds=(number - 1) * 1000).to_tuple_list() else: return [] - def calculate_this(self, unit): """ Calculates the date range for the current period specified by the unit (day, week, month, year, quarter). diff --git a/datespan/parser/lexer.py b/datespan/parser/lexer.py index a921883..1e43098 100644 --- a/datespan/parser/lexer.py +++ b/datespan/parser/lexer.py @@ -3,6 +3,7 @@ import re from dateutil import parser as dateutil_parser + from datespan.parser.errors import ParsingError @@ -138,7 +139,7 @@ class Lexer: 'qtd': 'qtd', 'wtd': 'wtd', - 'ltm': 'ltm', # last twelve months + 'ltm': 'ltm', # last twelve months 'py': 'py', # previous year 'cy': 'cy', # current year @@ -159,7 +160,7 @@ class Lexer: IDENTIFIER_ALIASES = { 'last': 'last', - 'previous': 'previous', # previous = last + 'previous': 'previous', # previous = last 'prev': 'previous', # prev = last 'prv': 'previous', # prev = last @@ -181,7 +182,7 @@ class Lexer: 'since': 'since', 'until': 'until', 'till': 'until', - 'up to': 'upto', # not yet implemented + 'up to': 'upto', # not yet implemented 'before': 'before', 'bef': 'before', @@ -216,7 +217,7 @@ class Lexer: # Regular expression for ordinals like '1st', '2nd', '3rd', '4th' ORDINAL_PATTERN = r'\b\d+(?:st|nd|rd|th)\b' - TRIPLET_PATTERN = r'^[rlpn](1000|[1-9][0-9]{0,2})[yqmwd]$' # r3m, l1q, p2w, n4d + TRIPLET_PATTERN = r'^[rlpn](1000|[1-9][0-9]{0,2})[yqmwd]$' # r3m, l1q, p2w, n4d # Regular expression for times, including optional 'am'/'pm', milliseconds, and microseconds TIME_PATTERN = ( @@ -242,19 +243,19 @@ class Lexer: TOKEN_SPECIFICATION = [ # Recognize datetime strings with optional 'am'/'pm', milliseconds, microseconds, and timezone - ('DATETIME', DATETIME_PATTERN), - ('DATE', DATE_PATTERN), # Date strings - ('TIME', TIME_PATTERN), # Time strings - ('ORDINAL', ORDINAL_PATTERN), # Ordinal numbers - ('NUMBER', r'\b\d+\b'), # Integer numbers - ('SPECIAL', r'\b(' + '|'.join(re.escape(k) for k in SPECIAL_WORDS_ALIASES.keys()) + r')\b'), # Special words - ('TRIPLET', TRIPLET_PATTERN), # triplet periods, like r3m, r4q, r6y, p2w, n4d - ('IDENTIFIER', r'\b(' + '|'.join(re.escape(k) for k in IDENTIFIER_ALIASES.keys()) + r')\b'), # Identifiers - ('TIME_UNIT', r'\b(' + '|'.join(re.escape(k) for k in TIME_UNIT_ALIASES.keys()) + r')\b'), # Time units - ('SEMICOLON', r';'), # Semicolon to separate statements + ('DATETIME', DATETIME_PATTERN), + ('DATE', DATE_PATTERN), # Date strings + ('TIME', TIME_PATTERN), # Time strings + ('ORDINAL', ORDINAL_PATTERN), # Ordinal numbers + ('NUMBER', r'\b\d+\b'), # Integer numbers + ('SPECIAL', r'\b(' + '|'.join(re.escape(k) for k in SPECIAL_WORDS_ALIASES.keys()) + r')\b'), # Special words + ('TRIPLET', TRIPLET_PATTERN), # triplet periods, like r3m, r4q, r6y, p2w, n4d + ('IDENTIFIER', r'\b(' + '|'.join(re.escape(k) for k in IDENTIFIER_ALIASES.keys()) + r')\b'), # Identifiers + ('TIME_UNIT', r'\b(' + '|'.join(re.escape(k) for k in TIME_UNIT_ALIASES.keys()) + r')\b'), # Time units + ('SEMICOLON', r';'), # Semicolon to separate statements ('PUNCTUATION', r'[,\-]'), # Commas and hyphens - ('SKIP', r'\s+'), # Skip over spaces and tabs - ('MISMATCH', r'.'), # Any other character + ('SKIP', r'\s+'), # Skip over spaces and tabs + ('MISMATCH', r'.'), # Any other character ] def __init__(self, text): @@ -270,8 +271,6 @@ def tokenize(self): Tokenizes the input text into a list of Token objects. """ - - tok_regex = '|'.join('(?P<%s>%s)' % pair for pair in self.TOKEN_SPECIFICATION) get_token = re.compile(tok_regex, re.IGNORECASE).match pos = 0 # Current position in the text @@ -282,7 +281,7 @@ def tokenize(self): tokens = [] for t in str(self.text).split(" "): if t.endswith(".") and len(t) > 1: - if t[-2].isalpha(): # e.g. 'prev.' + if t[-2].isalpha(): # e.g. 'prev.' t = t[:-1] tokens.append(t) self.text = " ".join(tokens) @@ -402,6 +401,7 @@ class Token: """ A simple Token structure with type, value, and position information. """ + def __init__(self, type_, value=None, line=1, column=1): self.type = type_ self.value = value # The actual value of the token (e.g., 'Monday', '1st') diff --git a/datespan/parser/parser.py b/datespan/parser/parser.py index d28b392..675f1a5 100644 --- a/datespan/parser/parser.py +++ b/datespan/parser/parser.py @@ -15,18 +15,21 @@ class DateSpanNode(ASTNode): """ Represents a date span node in the AST, which can be a specific date, relative period, or range. """ + def __init__(self, value): self.value = value # Dictionary containing details about the date span def __str__(self): return f"DateSpanNode({self.value})" + class Parser: """ The Parser class processes the list of tokens and builds an abstract syntax tree (AST). It follows the grammar rules to parse date expressions. """ - def __init__(self, tokens, text = None): + + def __init__(self, tokens, text=None): self.tokens = tokens self.text = text self.pos = 0 # Current position in the token list @@ -36,6 +39,7 @@ def __init__(self, tokens, text = None): def __str__(self): return f"Parser('{self.text}')" + def __repr__(self): return f"Parser('{self.text}')" @@ -51,7 +55,6 @@ def next_token(self): """ Returns the next token in the list. """ return self.tokens[self.pos + 1] if self.pos < len(self.tokens) - 1 else Token(TokenType.EOF, line=0, column=0) - def eat(self, token_type): """ Consumes the current token if it matches the expected token type. @@ -112,7 +115,7 @@ def parse_statement(self): node = self.date_span() date_spans.append(node) if self.current_token.type == TokenType.PUNCTUATION or \ - (self.current_token.type == TokenType.IDENTIFIER and self.current_token.value == 'and'): + (self.current_token.type == TokenType.IDENTIFIER and self.current_token.value == 'and'): self.eat(self.current_token.type) # Consume ',' or 'and' else: if self.current_token.type == TokenType.TIME: @@ -159,7 +162,7 @@ def date_span(self): elif self.current_token.type == TokenType.NUMBER or self.current_token.type == TokenType.ORDINAL: return self.relative_date_span() elif self.current_token.type == TokenType.TIME_UNIT: - if len(self.tokens) <=2 and self.tokens[-1].type == TokenType.EOF: + if len(self.tokens) <= 2 and self.tokens[-1].type == TokenType.EOF: # single word month, quarter, year, week, hour, minute, second or millisecond, handle as specials self.current_token.type = TokenType.SPECIAL return self.special_date_span() @@ -209,8 +212,8 @@ def iterative_date_span(self): # Collect period tokens period_tokens = [] while self.current_token.type != TokenType.EOF and \ - not (self.current_token.type == TokenType.PUNCTUATION or - self.current_token.type == TokenType.SEMICOLON): + not (self.current_token.type == TokenType.PUNCTUATION or + self.current_token.type == TokenType.SEMICOLON): period_tokens.append(self.current_token) self.eat(self.current_token.type) return DateSpanNode({'type': 'iterative', 'tokens': tokens, 'period_tokens': period_tokens}) @@ -238,7 +241,6 @@ def specific_time_span(self): self.eat(token_type) return DateSpanNode({'type': 'specific_date', 'date': time_value}) - def date_range(self): """ Parses a date range expression, such as 'from ... to ...' or 'between ... and ...'. @@ -248,7 +250,7 @@ def date_range(self): # Parse the start date expression start_tokens = [] while self.current_token.type != TokenType.EOF and \ - not (self.current_token.type == TokenType.IDENTIFIER and self.current_token.value in ['and', 'to']): + not (self.current_token.type == TokenType.IDENTIFIER and self.current_token.value in ['and', 'to']): start_tokens.append(self.current_token) self.eat(self.current_token.type) # Consume 'and' or 'to' @@ -267,9 +269,9 @@ def date_range(self): # Parse the end date expression end_tokens = [] while self.current_token.type != TokenType.EOF and \ - not (self.current_token.type == TokenType.PUNCTUATION or - self.current_token.type == TokenType.SEMICOLON or - (self.current_token.type == TokenType.IDENTIFIER and self.current_token.value == 'and')): + not (self.current_token.type == TokenType.PUNCTUATION or + self.current_token.type == TokenType.SEMICOLON or + (self.current_token.type == TokenType.IDENTIFIER and self.current_token.value == 'and')): end_tokens.append(self.current_token) self.eat(self.current_token.type) return DateSpanNode({'type': 'range', 'start_tokens': start_tokens, 'end_tokens': end_tokens}) @@ -281,8 +283,8 @@ def since_date_span(self): self.eat(TokenType.IDENTIFIER) # Consume 'since' tokens = [] while self.current_token.type != TokenType.EOF and \ - not (self.current_token.type == TokenType.PUNCTUATION or - self.current_token.type == TokenType.SEMICOLON): + not (self.current_token.type == TokenType.PUNCTUATION or + self.current_token.type == TokenType.SEMICOLON): tokens.append(self.current_token) self.eat(self.current_token.type) return DateSpanNode({'type': 'since', 'tokens': tokens}) @@ -296,8 +298,8 @@ def half_bound_date_span(self): self.eat(TokenType.IDENTIFIER) # Consume half bound keyword tokens = [] while self.current_token.type != TokenType.EOF and \ - not (self.current_token.type == TokenType.PUNCTUATION or - self.current_token.type == TokenType.SEMICOLON): + not (self.current_token.type == TokenType.PUNCTUATION or + self.current_token.type == TokenType.SEMICOLON): tokens.append(self.current_token) self.eat(self.current_token.type) return DateSpanNode({'type': 'half_bound', 'tokens': tokens, 'value': token.value}) @@ -307,7 +309,8 @@ def relative_date_span(self): Parses a relative date span, such as 'last week' or 'next 3 months'. """ tokens = [] - while self.current_token.type in [TokenType.IDENTIFIER, TokenType.NUMBER, TokenType.ORDINAL, TokenType.TIME_UNIT, TokenType.SPECIAL]: + while self.current_token.type in [TokenType.IDENTIFIER, TokenType.NUMBER, TokenType.ORDINAL, + TokenType.TIME_UNIT, TokenType.SPECIAL]: tokens.append(self.current_token) self.eat(self.current_token.type) return DateSpanNode({'type': 'relative', 'tokens': tokens}) diff --git a/pyproject.toml b/pyproject.toml index b16c884..62b6ea7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ Changelog = "https://github.com/Zeutschler/datespan/CHANGELOG.md" pypi = "https://pypi.org/project/datespan/" [tool.setuptools] -packages = ["datespan"] +packages = ["datespan", "datespan.tests"] [tool.setuptools.dynamic] version = {attr = "datespan.__version__"} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_class_DateSpan.py b/tests/test_class_DateSpan.py index 5af8127..dea2c16 100644 --- a/tests/test_class_DateSpan.py +++ b/tests/test_class_DateSpan.py @@ -2,8 +2,10 @@ import unittest from datetime import datetime, timedelta, time + from datespan import DateSpan + class TestDateSpan(unittest.TestCase): def setUp(self): @@ -169,7 +171,7 @@ def test_set_start(self): def test_set_end(self): new_end = datetime(2023, 1, 15) result = self.jan.set_end(day=15) - self.assertEqual(result.end, DateSpan(new_end).full_day.end ) + self.assertEqual(result.end, DateSpan(new_end).full_day.end) def test_set(self): new_date = datetime(2023, 1, 15) @@ -214,5 +216,6 @@ def test_parse_start_end_text(self): result = DateSpan('January 2023', 'March 2023') self.assertEqual(result, self.jan_feb_mar) + if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/test_class_DateSpanParser.py b/tests/test_class_DateSpanParser.py index e660134..9c89483 100644 --- a/tests/test_class_DateSpanParser.py +++ b/tests/test_class_DateSpanParser.py @@ -1,15 +1,14 @@ # datespan - Copyright (c)2024, Thomas Zeutschler, MIT license +import random import unittest from datetime import datetime, timedelta + from dateutil.relativedelta import relativedelta -import random -from datespan.parser.datespanparser import DateSpanParser -from datespan.parser.errors import ParsingError, EvaluationError from datespan import DateSpan - - +from datespan.parser.datespanparser import DateSpanParser +from datespan.parser.errors import EvaluationError class TestDateSpanParser(unittest.TestCase): @@ -51,7 +50,6 @@ def test_specific_date_span(self): self.assertEqual(end.date(), expected_date.date()) self.assertEqual(end.time(), datetime.max.time()) - def test_relative_date_span(self): """Test parsing of relative date spans.""" test_cases = [ @@ -79,13 +77,14 @@ def test_relative_date_span(self): # Calculate expected start date # todo: following test values are wrong -> use DateSpan if unit == 'day': - expected = DateSpan.today().full_day.shift_end(days=number) # today - timedelta(days=number) + expected = DateSpan.today().full_day.shift_end(days=number) # today - timedelta(days=number) elif unit == 'week': - expected = DateSpan.today().full_week.shift_end(weeks=number) # today - timedelta(weeks=number) + expected = DateSpan.today().full_week.shift_end(weeks=number) # today - timedelta(weeks=number) elif unit == 'month': - expected = DateSpan.today().full_month.shift_end(months=number) # today - relativedelta(months=number) + expected = DateSpan.today().full_month.shift_end( + months=number) # today - relativedelta(months=number) elif unit == 'year': - expected = DateSpan.today().full_year.shift_end(years=number) # today - relativedelta(years=number) + expected = DateSpan.today().full_year.shift_end(years=number) # today - relativedelta(years=number) else: continue self.assertEqual(start.date(), expected.start.date(), f"Input: '{input_text}' -> start = ") @@ -258,7 +257,7 @@ def test_multiple_statements(self): self.assertEqual(start.date(), yesterday.date()) # Third statement: last week start, end = date_spans[2][0] - expected_start = DateSpan.today().shift(days=-7).full_week.start # today - timedelta(weeks=1) + expected_start = DateSpan.today().shift(days=-7).full_week.start # today - timedelta(weeks=1) self.assertEqual(start.date(), expected_start.date()) def test_month_names(self): @@ -351,6 +350,7 @@ def test_invalid_date_format(self): with self.assertRaises(EvaluationError): parser.parse() + # Run the tests if __name__ == '__main__': - unittest.main(argv=[''], exit=False) \ No newline at end of file + unittest.main(argv=[''], exit=False) diff --git a/tests/test_class_DateSpanSet.py b/tests/test_class_DateSpanSet.py index 111cfe3..ed1a1d3 100644 --- a/tests/test_class_DateSpanSet.py +++ b/tests/test_class_DateSpanSet.py @@ -2,9 +2,11 @@ import unittest from datetime import datetime + from datespan.date_span import DateSpan from datespan.date_span_set import DateSpanSet + class TestDateSpanSet(unittest.TestCase): def setUp(self): @@ -157,5 +159,3 @@ def test_invalid_text(self): if __name__ == '__main__': unittest.main() - - diff --git a/tests/test_datespan_basics.py b/tests/test_datespan_basics.py index 3236c06..756e049 100644 --- a/tests/test_datespan_basics.py +++ b/tests/test_datespan_basics.py @@ -1,8 +1,9 @@ # datespan - Copyright (c)2024, Thomas Zeutschler, MIT license import sys +from datetime import datetime, time, timedelta from unittest import TestCase -from datetime import date, datetime, time, timedelta + from datespan.date_span import DateSpan @@ -103,11 +104,11 @@ def test_methods(self): self.assertEqual(now.full_second, DateSpan(now.start.replace(microsecond=0), now.end.replace(microsecond=999999))) self.assertEqual(now.full_minute, DateSpan(now.start.replace(second=0, microsecond=0), - now.end.replace(second=59, microsecond=999999))) + now.end.replace(second=59, microsecond=999999))) self.assertEqual(now.full_hour, DateSpan(now.start.replace(minute=0, second=0, microsecond=0), - now.end.replace(minute=59, second=59, microsecond=999999))) + now.end.replace(minute=59, second=59, microsecond=999999))) self.assertEqual(now.full_day, DateSpan(now.start.replace(hour=0, minute=0, second=0, microsecond=0), - now.end.replace(hour=23, minute=59, second=59, microsecond=999999))) + now.end.replace(hour=23, minute=59, second=59, microsecond=999999))) result = now.full_week # to lazy to write a test, copilot to stupid result = now.full_month # to lazy to write a test, copilot to stupid result = now.full_quarter # to lazy to write a test, copilot to stupid diff --git a/tests/test_datespan_methods.py b/tests/test_datespan_methods.py index a39a10f..0be15ba 100644 --- a/tests/test_datespan_methods.py +++ b/tests/test_datespan_methods.py @@ -2,8 +2,10 @@ import unittest from datetime import datetime, timedelta, time + from datespan.date_span import DateSpan + class TestDateSpan(unittest.TestCase): def setUp(self): @@ -12,7 +14,7 @@ def setUp(self): self.mar = DateSpan(datetime(2023, 3, 1), datetime(2023, 3, 31, 23, 59, 59, 999999)) self.jan_feb = DateSpan(datetime(2023, 1, 1), datetime(2023, 2, 28, 23, 59, 59, 999999)) self.jan_feb_mar = DateSpan(datetime(2023, 1, 1), datetime(2023, 3, 31, 23, 59, 59, 999999)) - self.today = DateSpan.today() # full day + self.today = DateSpan.today() # full day self.undef = DateSpan.undefined() def test_now(self): @@ -208,5 +210,6 @@ def test_le(self): def test_hash(self): self.assertEqual(hash(self.jan), hash((self.jan.start, self.jan.end))) + if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/test_datespanset.py b/tests/test_datespanset.py index aa94207..05ddf9e 100644 --- a/tests/test_datespanset.py +++ b/tests/test_datespanset.py @@ -1,13 +1,14 @@ # datespan - Copyright (c)2024, Thomas Zeutschler, MIT license import sys -from unittest import TestCase from datetime import datetime, time +from unittest import TestCase + import numpy as np import pandas as pd -from datespan.date_span_set import DateSpanSet from datespan.date_span import DateSpan +from datespan.date_span_set import DateSpanSet # from datespan.parser_old.en.tokenizer import Tokenizer @@ -156,7 +157,6 @@ def test_advanced(self): "from 2024-09-10 14:00:00.123 to 2024-09-10 15:00:00.789", "10/09/2024 14:00:00.123456", - "between 09/01/2024 and 09/10/2024", "from 09.01.2024 to 09.10.2024", "between 2024-09-01 and 2024-09-10", diff --git a/tests/test_debugging.py b/tests/test_debugging.py index fa11490..a1d9d77 100644 --- a/tests/test_debugging.py +++ b/tests/test_debugging.py @@ -1,9 +1,11 @@ # datespan - Copyright (c)2024, Thomas Zeutschler, MIT license from unittest import TestCase + from datespan.date_span import DateSpan + class TestDateTextParser(TestCase): def test_datespan_parsing(self): result = DateSpan('2023-01-01', '2023-01-31') - self.assertEqual(DateSpan('January 2023'), result) \ No newline at end of file + self.assertEqual(DateSpan('January 2023'), result)