diff --git a/datespanlib/__init__.py b/datespanlib/__init__.py index 70a8f0b..721a4c0 100644 --- a/datespanlib/__init__.py +++ b/datespanlib/__init__.py @@ -7,7 +7,7 @@ from datespanlib.date_span_set import DateSpanSet __author__ = "Thomas Zeutschler" -__version__ = "0.1.7" +__version__ = "0.1.8" __license__ = "MIT" VERSION = __version__ diff --git a/datespanlib/date_span.py b/datespanlib/date_span.py index ac2055d..31095ab 100644 --- a/datespanlib/date_span.py +++ b/datespanlib/date_span.py @@ -13,7 +13,7 @@ class DateSpan: The DateSpan is immutable, all methods that change the DateSpan will return a new DateSpan. """ - TIME_EPSILON_MICROSECONDS = 1000 # 1 millisecond + TIME_EPSILON_MICROSECONDS = 100_000 # 0.1 seconds """The time epsilon in microseconds used for comparison of time deltas.""" MIN_YEAR = 1700 """The minimum year that can be represented by the DateSpan.""" @@ -192,11 +192,11 @@ def with_time(self, time: datetime | time, text: str | None = None) -> DateSpan: if "." in parts[2]: return ds else: - return ds.full_second() + return ds.full_second elif len(parts) == 2: - return ds.full_minute() + return ds.full_minute elif len(parts) == 1: - return ds.full_hour() + return ds.full_hour return ds def with_start(self, dt: datetime) -> DateSpan: @@ -227,6 +227,7 @@ def with_year(self, year: int) -> DateSpan: year_diff = self._end.year - self._start.year return DateSpan(self._start.replace(year=year), self._end.replace(year=year + year_diff)) + @property def full_millisecond(self) -> DateSpan: """ Returns a new DateSpan with the start and end date set to the beginning and end of the respective millisecond(s). @@ -235,6 +236,7 @@ def full_millisecond(self) -> DateSpan: return DateSpan(self._start.replace(microsecond=musec), self._end.replace(microsecond=musec + 999)) + @property def full_second(self) -> DateSpan: """ Returns a new DateSpan with the start and end date set to the beginning and end of the respective second(s). @@ -242,6 +244,7 @@ def full_second(self) -> DateSpan: return DateSpan(self._start.replace(microsecond=0), self._end.replace(microsecond=999999)) + @property def full_minute(self) -> DateSpan: """ Returns a new DateSpan with the start and end date set to the beginning and end of the respective minute(s). @@ -249,6 +252,7 @@ def full_minute(self) -> DateSpan: return DateSpan(self._start.replace(second=0, microsecond=0), self._end.replace(second=59, microsecond=999999)) + @property def full_hour(self) -> DateSpan: """ Returns a new DateSpan with the start and end date set to the beginning and end of the respective hour(s). @@ -256,13 +260,19 @@ def full_hour(self) -> DateSpan: return DateSpan(self._start.replace(minute=0, second=0, microsecond=0), self._end.replace(minute=59, second=59, microsecond=999999)) + @property def full_day(self) -> DateSpan: """ Returns a new DateSpan with the start and end date set to the beginning and end of the respective day(s). """ + 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)) + return DateSpan(self._start.replace(hour=0, minute=0, second=0, microsecond=0), self._end.replace(hour=23, minute=59, second=59, microsecond=999999)) + @property def full_week(self) -> DateSpan: """ Returns a new DateSpan with the start and end date set to the beginning and end of the respective week(s). @@ -272,6 +282,7 @@ def full_week(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 full_month(self) -> DateSpan: """ Returns a new DateSpan with the start and end date set to the beginning and end of the respective month(s). @@ -281,6 +292,7 @@ def full_month(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 full_quarter(self) -> DateSpan: """ Returns a new DateSpan with the start and end date set to the beginning and end of the respective quarter(s). @@ -294,6 +306,7 @@ def full_quarter(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 full_year(self) -> DateSpan: """ Returns a new DateSpan with the start and end date set to the beginning and end of the respective year(s). @@ -303,45 +316,61 @@ 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)) - @staticmethod - def ltm() -> DateSpan: + + @property + def ltm(self) -> DateSpan: """ - Returns a new DateSpan representing the last 12 months. The start date will be set to the first day of the + Returns a new DateSpan representing the last 12 months relative to the end date of the DateSpan. + If the DateSpan is undefined, the last 12 months relative to today will be returned. """ - ds = DateSpan.today().shift_start(years=-1, days=1) + if self.is_undefined: + return DateSpan.today().shift_start(years=-1, days=1) + ds = DateSpan(self._end, self._end).shift_start(years=-1, days=1) return ds - @staticmethod - def ytd() -> DateSpan: + @property + def ytd(self) -> DateSpan: """ - Returns a new DateSpan with the start and end date set to the beginning and end of the year-to-date. + Returns a new DateSpan with the start set to the beginning of the current DateSpan's start year and the end + set to the end of the current end date of the DateSpan. + If the DateSpan is undefined, the beginning of the current year up to today (full day) will be returned. """ - ds = DateSpan.today() - return ds.with_start(ds.full_year().start) + 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)) - @staticmethod - def mtd() -> DateSpan: + @property + def mtd(self) -> DateSpan: """ Returns a new DateSpan with the start and end date set to the beginning and end of the month-to-date. """ - ds = DateSpan.today() - return ds.with_start(ds.full_month().start) + 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)) + - @staticmethod - def qtd() -> DateSpan: + @property + def qtd(self) -> DateSpan: """ Returns a new DateSpan with the start and end date set to the beginning and end of the quarter-to-date. """ - ds = DateSpan.today() - return ds.with_start(ds.full_quarter().start) + 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)) + - @staticmethod - def wtd() -> DateSpan: + @property + def wtd(self) -> DateSpan: """ Returns a new DateSpan with the start and end date set to the beginning and end of the week-to-date. """ - ds = DateSpan.today() - return ds.with_start(ds.full_week()._start) + 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)) def _begin_of_day(self, dt: datetime) -> datetime: """Returns the beginning of the day for the given datetime.""" @@ -383,11 +412,44 @@ def begins_on_month_start(self) -> bool: @property def is_full_month(self) -> bool: """ - Returns True if the DateSpan is a full month. + Returns True if the DateSpan represents one or more full months. """ return (self._start == self._begin_of_month(self._start) and self._end == self._end_of_month(self._end)) + @property + def is_full_quarter(self) -> bool: + """ + Returns True if the DateSpan represents one or more full quarters. + """ + return (self._start == self._begin_of_month(self._start) and self._start.month % 3 == 1 and + self._end == self._end_of_month(self._end) and self._end.month % 3 == 0) + + @property + def is_full_year(self) -> bool: + """ + Returns True if the DateSpan represents one or more full year. + """ + return (self._start == self._start.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0) and + self._end == self._end.replace(month=12, day=31, hour=23, minute=59, second=59, microsecond=999999)) + + @property + def is_full_week(self) -> bool: + """ + Returns True if the DateSpan represents one or more full weeks. + """ + return (self._start == self._begin_of_day(self._start - timedelta(days=self._start.weekday())) and + self._end == self._end_of_day(self._end + timedelta(days=6 - self._end.weekday()))) + + @property + def is_full_day(self) -> bool: + """ + Returns True if the DateSpan represents one or more full days. + """ + return (self._start == self._begin_of_day(self._start) and + self._end == self._end_of_day(self._end)) + + def _swap(self) -> DateSpan: """Swap start and end date if start is greater than end.""" if self._start is None or self._end is None: @@ -565,7 +627,7 @@ def _set(self, dt: datetime, year: int | None = None, month: int | None = None, if day is not None: if not 0 < day < 32: raise ValueError(f"Invalid day value '{day}'.") - last_day = DateSpan(self._start).full_month()._end.day + last_day = DateSpan(self._start).full_month._end.day if day > last_day: day = last_day dt = dt.replace(day=day) @@ -615,6 +677,13 @@ def to_tuple_list(self) -> list[tuple[datetime, datetime]]: # region Static Days, Month and other calculations + @classmethod + def max(cls) -> DateSpan: + """ + Returns the maximum possible DateSpan ranging from datetime.min to datetime.max. + """ + return DateSpan(datetime.min, datetime.max) + @classmethod def now(cls) -> DateSpan: """Returns a new DateSpan with the start and end date set to the current date and time.""" @@ -624,125 +693,208 @@ def now(cls) -> DateSpan: @classmethod def today(cls) -> DateSpan: """Returns a new DateSpan with the start and end date set to the current date.""" - return DateSpan.now().full_day() + return DateSpan.now().full_day @classmethod def yesterday(cls) -> DateSpan: """Returns a new DateSpan with the start and end date set to yesterday.""" - return DateSpan.now().shift(days=-1).full_day() + return DateSpan.now().shift(days=-1).full_day @classmethod def tomorrow(cls) -> DateSpan: """Returns a new DateSpan with the start and end date set to tomorrow.""" - return DateSpan.now().shift(days=1).full_day() + return DateSpan.now().shift(days=1).full_day @classmethod def undefined(cls) -> DateSpan: - """Returns an empty / undefined DateSpan.""" + """Returns an undefined DateSpan. Same as `span = DateSpan()`.""" return DateSpan(None, None) @classmethod - def _monday(cls, 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 - dtv = datetime.now() + relativedelta(weekday=MO(-1), years=offset_years, + 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) - return DateSpan(dtv).full_day() + return DateSpan(dtv).full_day - @classmethod - def monday(cls): - """Returns a full day DateSpan for the current weeks Monday.""" - return cls._monday() + @property + 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) - @classmethod - def tuesday(cls): - """Returns a full day DateSpan for the current weeks Tuesday.""" - return cls._monday().shift(days=1) + @property + 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) - @classmethod - def wednesday(cls): - """Returns a full day DateSpan for the current weeks Wednesday.""" - return cls._monday().shift(days=2) + @property + 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) - @classmethod - def thursday(cls): - """Returns a full day DateSpan for the current weeks Thursday.""" - return cls._monday().shift(days=3) + @property + 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) - @classmethod - def friday(cls): - """Returns a full day DateSpan for the current weeks Friday.""" - return cls._monday().shift(days=4) + @property + 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) - @classmethod - def saturday(cls): - """Returns a full day DateSpan for the current weeks Saturday.""" - return cls._monday().shift(days=5) + @property + 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) - @classmethod - def sunday(cls): - """Returns a full day DateSpan for the current weeks Sunday.""" - return cls._monday().shift(days=6) + @property + 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) - @classmethod - def january(cls): - """Returns a full month DateSpan for January of the current year.""" - return DateSpan.now().replace(month=2).full_month() - @classmethod - def february(cls): - """Returns a full month DateSpan for February of the current year.""" - return DateSpan.now().replace(month=2).full_month() + @property + def january(self): + """ + Returns a full month DateSpan for January relative to the start date time of the DateSpan. + If the DateSpan is undefined, the current year's January will be returned. + """ + if self.is_undefined: + return DateSpan.now().replace(month=1).full_month + return self._start.replace(month=1).full_month - @classmethod - def march(cls): - """Returns a full month DateSpan for March of the current year.""" - return DateSpan.now().replace(month=3).full_month() + @property + def february(self): + """ + Returns a full month DateSpan for February relative to the start date time of the DateSpan. + If the DateSpan is undefined, the current year's February will be returned. + """ + if self.is_undefined: + return DateSpan.now().replace(month=2).full_month + return self._start.replace(month=2).full_month - @classmethod - def april(cls): - """Returns a full month DateSpan for April of the current year.""" - return DateSpan.now().replace(month=4).full_month() + @property + def march(self): + """ + Returns a full month DateSpan for March relative to the start date time of the DateSpan. + If the DateSpan is undefined, the current year's March will be returned. + """ + if self.is_undefined: + return DateSpan.now().replace(month=3).full_month + return self._start.replace(month=3).full_month - @classmethod - def may(cls): - """Returns a full month DateSpan for May of the current year.""" - return DateSpan.now().replace(month=5).full_month() + @property + def april(self): + """ + Returns a full month DateSpan for April relative to the start date time of the DateSpan. + If the DateSpan is undefined, the current year's April will be returned. + """ + if self.is_undefined: + return DateSpan.now().replace(month=4).full_month + return self._start.replace(month=4).full_month - @classmethod - def june(cls): - """Returns a full month DateSpan for June of the current year.""" - return DateSpan.now().replace(month=6).full_month() + @property + def may(self): + """ + Returns a full month DateSpan for May relative to the start date time of the DateSpan. + If the DateSpan is undefined, the current year's May will be returned. + """ + if self.is_undefined: + return DateSpan.now().replace(month=5).full_month + return self._start.replace(month=5).full_month - @classmethod - def july(cls): - """Returns a full month DateSpan for July of the current year.""" - return DateSpan.now().replace(month=7).full_month() + @property + def june(self): + """ + Returns a full month DateSpan for June relative to the start date time of the DateSpan. + If the DateSpan is undefined, the current year's June will be returned. + """ + if self.is_undefined: + return DateSpan.now().replace(month=6).full_month + return self._start.replace(month=6).full_month - @classmethod - def august(cls): - """Returns a full month DateSpan for August of the current year.""" - return DateSpan.now().replace(month=8).full_month() + @property + def july(self): + """ + Returns a full month DateSpan for July relative to the start date time of the DateSpan. + If the DateSpan is undefined, the current year's July will be returned. + """ + if self.is_undefined: + return DateSpan.now().replace(month=7).full_month + return self._start.replace(month=7).full_month - @classmethod - def september(cls): - """Returns a full month DateSpan for September of the current year.""" - return DateSpan.now().replace(month=9).full_month() + @property + def august(self): + """ + Returns a full month DateSpan for August relative to the start date time of the DateSpan. + If the DateSpan is undefined, the current year's August will be returned. + """ + if self.is_undefined: + return DateSpan.now().replace(month=8).full_month + return self._start.replace(month=8).full_month - @classmethod - def october(cls): - """Returns a full month DateSpan for October of the current year.""" - return DateSpan.now().replace(month=10).full_month() + @property + def september(self): + """ + Returns a full month DateSpan for September relative to the start date time of the DateSpan. + If the DateSpan is undefined, the current year's September will be returned. + """ + if self.is_undefined: + return DateSpan.now().replace(month=9).full_month + return self._start.replace(month=9).full_month - @classmethod - def november(cls): - """Returns a full month DateSpan for November of the current year.""" - return DateSpan.now().replace(month=11).full_month() + @property + def october(self): + """ + Returns a full month DateSpan for October relative to the start date time of the DateSpan. + If the DateSpan is undefined, the current year's October will be returned. + """ + if self.is_undefined: + return DateSpan.now().replace(month=10).full_month + return self._start.replace(month=10).full_month - @classmethod - def december(cls): - """Returns a full month DateSpan for December of the current year.""" - return DateSpan.now().replace(month=12).full_month() + @property + def november(self): + """ + Returns a full month DateSpan for November relative to the start date time of the DateSpan. + If the DateSpan is undefined, the current year's November will be returned. + """ + if self.is_undefined: + return DateSpan.now().replace(month=11).full_month + return self._start.replace(month=11).full_month + @property + def december(self): + """ + Returns a full month DateSpan for December relative to the start date time of the DateSpan. + If the DateSpan is undefined, the current year's December will be returned. + """ + if self.is_undefined: + return DateSpan.now().replace(month=12).full_month + return self._start.replace(month=12).full_month # endregion # region magic methods diff --git a/datespanlib/parser/evaluator.py b/datespanlib/parser/evaluator.py index ab74fe5..7d48b19 100644 --- a/datespanlib/parser/evaluator.py +++ b/datespanlib/parser/evaluator.py @@ -59,8 +59,11 @@ def evaluate_node(self, node): elif node_type == 'range': return self.evaluate_range(node.value['start_tokens'], node.value['end_tokens']) - elif node_type == 'since': - return self.evaluate_since(node.value['tokens']) + # elif node_type == 'since': + # return self.evaluate_since(node.value['tokens']) + elif node_type == 'half_bound': + return self.evaluate_half_bound(node.value['tokens'], node.value['value']) + elif node_type == 'iterative': return self.evaluate_iterative(node.value['tokens'], node.value['period_tokens']) else: @@ -171,6 +174,57 @@ def evaluate_since(self, tokens): end_date = self.today return [(start_date, end_date)] + def evaluate_half_bound(self, tokens, keyword): + """ + Evaluates a half bounded expression, calculating the date range from or to a specified date/time. + since - from date to now + after - from date to date max + before - from date min to date + until - from date min to date + """ + # Parse the date/time expression following 'since', 'after', 'before', or 'until' + if len(tokens) == 0: + raise EvaluationError(f"Date of date span missing after " + f"'since', 'after', 'before', or 'until'.") + + parser = Parser(tokens + [Token(TokenType.EOF)]) + 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}") + if not ast_nodes: + raise EvaluationError(f"Date of date span missing after " + f"'since', 'after', 'before', or 'until'.") + + try: + spans = self.evaluate_node(ast_nodes[0]) + except EvaluationError as e: + raise EvaluationError(f"Failed to evaluate date of date span after " + f"'since', 'after', 'before', or 'until': {e}") + if not spans: + raise EvaluationError(f"Failed to evaluate date of date span after " + f"'since', 'after', 'before', or 'until'.") + + if keyword == 'since': + start_date = spans[0][0] + end_date = self.today + elif keyword == 'from': + start_date = spans[0][0] + end_date = datetime.max + elif keyword == 'after': + start_date = spans[0][1] + timedelta(microseconds=1) + end_date = datetime.max + elif keyword in ['before', 'until']: + start_date = datetime.min + end_date = spans[0][0] - timedelta(microseconds=1) + else: + raise EvaluationError( + f"Failed to evaluate date or datespan. Expected 'since', " + 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. @@ -335,33 +389,33 @@ def evaluate_special(self, value): elif value == 'now': return DateSpan.now().to_tuple_list() elif value == 'ltm': - return DateSpan.ltm().to_tuple_list() + return DateSpan().ltm.to_tuple_list() elif value == 'ytd': - return DateSpan.ytd().to_tuple_list() + return DateSpan().ytd.to_tuple_list() elif value == 'qtd': - return DateSpan.qtd().to_tuple_list() + return DateSpan().qtd.to_tuple_list() elif value == 'mtd': - return DateSpan.mtd().to_tuple_list() + return DateSpan().mtd.to_tuple_list() elif value == 'wtd': - return DateSpan.wtd().to_tuple_list() + return DateSpan().wtd.to_tuple_list() # catch the following single words as specials elif value == 'week': - return DateSpan.now().full_week().to_tuple_list() + return DateSpan.now().full_week.to_tuple_list() elif value == 'month': - return DateSpan.now().full_month().to_tuple_list() + return DateSpan.now().full_month.to_tuple_list() elif value == 'year': - return DateSpan.now().full_year().to_tuple_list() + return DateSpan.now().full_year.to_tuple_list() elif value == 'quarter': - return DateSpan.now().full_quarter().to_tuple_list() + return DateSpan.now().full_quarter.to_tuple_list() elif value == 'hour': - return DateSpan.now().full_hour().to_tuple_list() + return DateSpan.now().full_hour.to_tuple_list() elif value == 'minute': - return DateSpan.now().full_minute().to_tuple_list() + return DateSpan.now().full_minute.to_tuple_list() elif value == 'second': - return DateSpan.now().full_second().to_tuple_list() + return DateSpan.now().full_second.to_tuple_list() elif value == 'millisecond': - return DateSpan.now().full_millisecond().to_tuple_list() + return DateSpan.now().full_millisecond.to_tuple_list() elif value in ['q1', 'q2', 'q3', 'q4']: @@ -369,15 +423,15 @@ def evaluate_special(self, value): 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() + return DateSpan.now().shift(years=-1).full_year.to_tuple_list() elif value == 'cy': - return DateSpan.now().full_year().to_tuple_list() + return DateSpan.now().full_year.to_tuple_list() elif value == 'ny': - return DateSpan.now().shift(years=1).full_year().to_tuple_list() + return DateSpan.now().shift(years=1).full_year.to_tuple_list() elif value == 'ly': - return DateSpan.now().shift(years=-1).full_year().to_tuple_list() + return DateSpan.now().shift(years=-1).full_year.to_tuple_list() else: return [] @@ -448,11 +502,22 @@ def evaluate_days(self, tokens): days.append(day_full_name) idx += 1 date_spans = [] - lookups = {"monday": DateSpan.monday, "tuesday": DateSpan.tuesday, "wednesday": DateSpan.wednesday, - "thursday": DateSpan.thursday, "friday": DateSpan.friday, "saturday": DateSpan.saturday, - "sunday": DateSpan.sunday} + for day_name in days: - span = lookups[day_name]() + if day_name == 'monday': + span = DateSpan().monday + elif day_name == 'tuesday': + span = DateSpan().tuesday + elif day_name == 'wednesday': + span = DateSpan().wednesday + elif day_name == 'thursday': + span = DateSpan().thursday + elif day_name == 'friday': + span = DateSpan().friday + elif day_name == 'saturday': + span = DateSpan().saturday + elif day_name == 'sunday': + span = DateSpan().sunday date_spans.append((span.start, span.end)) return date_spans @@ -469,7 +534,7 @@ def calculate_rolling(self, number, unit): elif unit == 'quarter': return DateSpan.now().shift_start(months=-number * 3).to_tuple_list() elif unit == 'week': - return DateSpan.now().shift(weeks=-1).full_week().shift_start(weeks=number - 1).to_tuple_list() + return DateSpan.now().shift(weeks=-1).full_week.shift_start(weeks=number - 1).to_tuple_list() elif unit == 'day': return DateSpan.now().shift_start(days=-number).to_tuple_list() elif unit == 'hour': @@ -490,23 +555,23 @@ def calculate_previous(self, number, unit): Note: Previous and last are synonyms. """ if unit == 'month': # most used units first - return DateSpan.today().shift(months=-1).full_month().shift_start(months=-(number - 1)).to_tuple_list() + 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() + return DateSpan.today().shift(years=-1).full_year.shift_start(years=-(number - 1)).to_tuple_list() elif unit == 'quarter': - return DateSpan.today().shift(months=-3).full_quarter().shift_start(months=-(number - 1) * 3).to_tuple_list() + return DateSpan.today().shift(months=-3).full_quarter.shift_start(months=-(number - 1) * 3).to_tuple_list() elif unit == 'week': - return DateSpan.today().shift(weeks=-1).full_week().shift_start(weeks=-(number - 1)).to_tuple_list() + return DateSpan.today().shift(weeks=-1).full_week.shift_start(weeks=-(number - 1)).to_tuple_list() elif unit == 'day': return DateSpan.yesterday().shift_start(days=-(number - 1)).to_tuple_list() elif unit == 'hour': - return DateSpan.now().shift(hours=-1).full_hour().shift_start(hours=-(number - 1)).to_tuple_list() + return DateSpan.now().shift(hours=-1).full_hour.shift_start(hours=-(number - 1)).to_tuple_list() elif unit == 'minute': - return DateSpan.now().shift(minutes=-1).full_minute().shift_start(minutes=-(number - 1)).to_tuple_list() + return DateSpan.now().shift(minutes=-1).full_minute.shift_start(minutes=-(number - 1)).to_tuple_list() elif unit == 'second': - return DateSpan.now().shift(seconds=-1).full_second().shift_start(seconds=-(number - 1)).to_tuple_list() + 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 [] @@ -518,21 +583,21 @@ def calculate_future(self, number, unit): if unit == 'day': return DateSpan.today().shift(days=1).shift_end(days=(number - 1)).to_tuple_list() elif unit == 'week': - return DateSpan.today().shift(weeks=1).full_week().shift_end(weeks=(number - 1)).to_tuple_list() + return DateSpan.today().shift(weeks=1).full_week.shift_end(weeks=(number - 1)).to_tuple_list() elif unit == 'month': - return DateSpan.today().shift(months=1).full_month().shift_end(months=(number - 1)).to_tuple_list() + return DateSpan.today().shift(months=1).full_month.shift_end(months=(number - 1)).to_tuple_list() elif unit == 'year': - return DateSpan.today().shift(years=1).full_year().shift_end(years=(number - 1)).to_tuple_list() + 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() + return DateSpan.now().shift(hours=1).full_hour.shift_end(hours=number - 1).to_tuple_list() elif unit == 'minute': - return DateSpan.now().shift(minutes=1).full_minute().shift_end(minutes=number - 1).to_tuple_list() + return DateSpan.now().shift(minutes=1).full_minute.shift_end(minutes=number - 1).to_tuple_list() elif unit == 'second': - return DateSpan.now().shift(seconds=1).full_second().shift_end(seconds=number - 1).to_tuple_list() + 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 [] @@ -542,23 +607,23 @@ def calculate_this(self, unit): Calculates the date range for the current period specified by the unit (day, week, month, year, quarter). """ if unit == 'day': - return DateSpan.now().full_day().to_tuple_list() + return DateSpan.now().full_day.to_tuple_list() elif unit == 'week': - return DateSpan.now().full_week().to_tuple_list() + return DateSpan.now().full_week.to_tuple_list() elif unit == 'month': - return DateSpan.now().full_month().to_tuple_list() + return DateSpan.now().full_month.to_tuple_list() elif unit == 'year': - return DateSpan.now().full_year().to_tuple_list() + return DateSpan.now().full_year.to_tuple_list() elif unit == 'quarter': - return DateSpan.now().full_quarter().to_tuple_list() + return DateSpan.now().full_quarter.to_tuple_list() elif unit == 'hour': - return DateSpan.now().full_hour().to_tuple_list() + return DateSpan.now().full_hour.to_tuple_list() elif unit == 'minute': - return DateSpan.now().full_minute().to_tuple_list() + return DateSpan.now().full_minute.to_tuple_list() elif unit == 'second': - return DateSpan.now().full_second().to_tuple_list() + return DateSpan.now().full_second.to_tuple_list() elif unit == 'millisecond': - return DateSpan.now().full_millisecond().to_tuple_list() + return DateSpan.now().full_millisecond.to_tuple_list() else: return [] diff --git a/datespanlib/parser/lexer.py b/datespanlib/parser/lexer.py index 8ee2d58..db290fa 100644 --- a/datespanlib/parser/lexer.py +++ b/datespanlib/parser/lexer.py @@ -172,10 +172,24 @@ class Lexer: 'next': 'next', - 'since': 'since', 'and': 'and', 'of': 'of', - 'in': 'in', # Added 'in' to IDENTIFIER_ALIASES + 'in': 'in', + + 'since': 'since', + 'until': 'until', + 'till': 'until', + 'up to': 'upto', # not yet implemented + + 'before': 'before', + 'bef': 'before', + 'bfr': 'before', + 'ante': 'before', + + 'after': 'after', + 'aft': 'after', + 'aftr': 'after', + 'post': 'after', 'from': 'from', 'frm': 'from', diff --git a/datespanlib/parser/parser.py b/datespanlib/parser/parser.py index 8f33843..c7dac48 100644 --- a/datespanlib/parser/parser.py +++ b/datespanlib/parser/parser.py @@ -1,3 +1,5 @@ +from anyio.lowlevel import current_token + from datespanlib.parser.errors import ParsingError from datespanlib.parser.lexer import Token, TokenType, Lexer @@ -100,10 +102,12 @@ def date_span(self): if self.current_token.type == TokenType.IDENTIFIER: if self.current_token.value == 'every': return self.iterative_date_span() - elif self.current_token.value in ['last', 'next', 'past', 'this', 'previous', 'rolling']: + elif self.current_token.value in ['last', 'next', 'past', 'previous', 'rolling', 'this']: return self.relative_date_span() - elif self.current_token.value == 'since': - return self.since_date_span() + # elif self.current_token.value == 'since': + # return self.since_date_span() + elif self.current_token.value in ['after', 'before', 'since', 'until']: + return self.half_bound_date_span() elif self.current_token.value in ['between', 'from']: return self.date_range() elif self.current_token.value in Lexer.MONTH_ALIASES.values(): @@ -215,6 +219,7 @@ def date_range(self): """ Parses a date range expression, such as 'from ... to ...' or 'between ... and ...'. """ + token = self.current_token self.eat(TokenType.IDENTIFIER) # Consume 'from' or 'between' # Parse the start date expression start_tokens = [] @@ -226,6 +231,9 @@ def date_range(self): if self.current_token.type == TokenType.IDENTIFIER and self.current_token.value in ['and', 'to']: self.eat(TokenType.IDENTIFIER) else: + if token.value == 'from': + # special case, 'from' without 'to' or 'and' + return DateSpanNode({'type': 'half_bound', 'tokens': start_tokens, 'value': token.value}) raise ParsingError( f"Expected 'and' or 'to', got '{self.current_token.value!r}'", self.current_token.line, @@ -255,6 +263,21 @@ def since_date_span(self): self.eat(self.current_token.type) return DateSpanNode({'type': 'since', 'tokens': tokens}) + def half_bound_date_span(self): + """ + Parses date time spans with a one side bound like 'since', 'before', 'after' date expression, + e.g. as in 'since August 2024', 'after Friday' or 'before 2024'. + """ + token = self.current_token + 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): + tokens.append(self.current_token) + self.eat(self.current_token.type) + return DateSpanNode({'type': 'half_bound', 'tokens': tokens, 'value': token.value}) + def relative_date_span(self): """ Parses a relative date span, such as 'last week' or 'next 3 months'. diff --git a/tests/test_class_DateSpan.py b/tests/test_class_DateSpan.py index 33fca06..6e6dd8e 100644 --- a/tests/test_class_DateSpan.py +++ b/tests/test_class_DateSpan.py @@ -84,69 +84,68 @@ def test_with_year(self): self.assertEqual(result.end.year, 2024) def test_full_millisecond(self): - result = self.jan.full_millisecond() + result = self.jan.full_millisecond self.assertEqual(result.start.microsecond % 1000, 0) self.assertEqual(result.end.microsecond % 1000, 999) def test_full_second(self): - result = self.jan.full_second() + result = self.jan.full_second self.assertEqual(result.start.microsecond, 0) self.assertEqual(result.end.microsecond, 999999) def test_full_minute(self): - result = self.jan.full_minute() + result = self.jan.full_minute self.assertEqual(result.start.second, 0) self.assertEqual(result.end.second, 59) def test_full_hour(self): - result = self.jan.full_hour() + result = self.jan.full_hour self.assertEqual(result.start.minute, 0) self.assertEqual(result.end.minute, 59) def test_full_day(self): - result = self.jan.full_day() + result = self.jan.full_day self.assertEqual(result.start.hour, 0) self.assertEqual(result.end.hour, 23) def test_full_week(self): - result = self.jan.full_week() + result = self.jan.full_week self.assertEqual(result.start.weekday(), 0) self.assertEqual(result.end.weekday(), 6) def test_full_month(self): - result = self.jan.full_month() + result = self.jan.full_month self.assertEqual(result.start.day, 1) self.assertEqual(result.end.day, 31) def test_full_quarter(self): - result = self.jan.full_quarter() + result = self.jan.full_quarter self.assertEqual(result.start.month, 1) self.assertEqual(result.end.month, 3) def test_full_year(self): - result = self.jan.full_year() + result = self.jan.full_year self.assertEqual(result.start.month, 1) self.assertEqual(result.end.month, 12) def test_ytd(self): - result = self.jan.ytd() + result = self.jan.ytd self.assertEqual(result.start.month, 1) - self.assertEqual(result.end, DateSpan.ytd().end) + self.assertEqual(result.end, self.jan.end) def test_mtd(self): - result = self.jan.mtd() + result = self.jan.mtd self.assertEqual(result.start.day, 1) - self.assertEqual(result.end.day, DateSpan.today().end.day) + self.assertEqual(result.end.day, self.jan.end.day) def test_qtd(self): - result = self.jan.qtd() - self.assertEqual(result, DateSpan.qtd()) - self.assertEqual(result.end.day, DateSpan.today().end.day) + result = self.jan.qtd + self.assertEqual(result.start.day, 1) + self.assertEqual(result.end.day, self.jan.end.day) def test_wtd(self): - result = self.jan.wtd() - self.assertEqual(result, DateSpan.wtd()) - self.assertEqual(result.end.day, DateSpan.today().end.day) + result = self.jan.wtd + self.assertEqual(result.end.day, self.jan.end.day) def test_shift(self): result = self.jan.shift(months=1) @@ -168,13 +167,13 @@ 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) result = self.jan.set(day=15) - self.assertEqual(result.start, DateSpan(new_date).full_day().start) - self.assertEqual(result.end, DateSpan(new_date).full_day().end) + self.assertEqual(result.start, DateSpan(new_date).full_day.start) + self.assertEqual(result.end, DateSpan(new_date).full_day.end) def test_duration(self): self.assertAlmostEqual(self.jan.duration, 30.999988425925926, places=4) diff --git a/tests/test_class_DateSpanParser.py b/tests/test_class_DateSpanParser.py index c868dc7..ebb9a55 100644 --- a/tests/test_class_DateSpanParser.py +++ b/tests/test_class_DateSpanParser.py @@ -77,13 +77,13 @@ 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 = ") @@ -104,17 +104,24 @@ def test_iterative_date_span(self): self.assertEqual(start.month, datetime.today().month) self.assertEqual(end.date(), start.date()) - def test_since_keyword(self): + def test_half_bounded_keywords(self): """Test parsing of 'since' keyword.""" - input_text = "since August 2024" - parser = DateSpanParser(input_text) - parser.parse() - date_spans = parser.date_spans - self.assertEqual(len(date_spans), 1) - start, end = date_spans[0][0] - self.assertEqual(start, datetime(2024, 8, 1)) - # End should be today - self.assertEqual(end.date(), datetime.today().date()) + aug24 = DateSpan(datetime(2024, 8, 1)).full_month + texts = [("since August 2024", aug24.start, DateSpan().now().end), + ("after August 2024", aug24.end + timedelta(microseconds=1), DateSpan.max().end), + ("until August 2024", DateSpan().max().start, aug24.start - timedelta(microseconds=1)), + ("from August 2024", aug24.start, DateSpan().max().end), + ("before August 2024", DateSpan().max().start, aug24.start - timedelta(microseconds=1)), + ("till August 2024", DateSpan().max().start, aug24.start - timedelta(microseconds=1)), + # ("upto August 2024", DateSpan().today().start, datetime(2024, 8, 1)), + ] + for input_text, tobe_start, tobe_end in texts: + parser = DateSpanParser(input_text) + parser.parse() + date_span = parser.date_spans[0][0] + as_is: DateSpan = DateSpan(date_span[0], date_span[1]) + to_be: DateSpan = DateSpan(tobe_start, tobe_end) + self.assertTrue(as_is.almost_equals(to_be), f"Input: '{input_text}' -> {as_is} != {to_be}") def test_now_keyword(self): """Test parsing of 'now' keyword.""" @@ -249,7 +256,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): diff --git a/tests/test_datespan_basics.py b/tests/test_datespan_basics.py index d997f59..46ddd47 100644 --- a/tests/test_datespan_basics.py +++ b/tests/test_datespan_basics.py @@ -40,9 +40,9 @@ def test_methods(self): self.assertTrue(DateSpan(dt1, dt2).consecutive_with(DateSpan(dt2, dt3))) self.assertTrue(DateSpan(dt1, dt2).overlaps_with(DateSpan(dt1, dt3))) - jan = DateSpan.now().replace(month=1).full_month() - feb = DateSpan.now().replace(month=2).full_month() - mar = DateSpan.now().replace(month=3).full_month() + jan = DateSpan.now().replace(month=1).full_month + feb = DateSpan.now().replace(month=2).full_month + mar = DateSpan.now().replace(month=3).full_month # DateSpan arithmetic jan_feb = jan + feb @@ -101,18 +101,18 @@ def test_methods(self): jan.end.replace(hour=12, minute=34, second=56, microsecond=0)) self.assertTrue(result == to_be) - self.assertEqual(now.full_second(), + 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), + self.assertEqual(now.full_minute, DateSpan(now.start.replace(second=0, microsecond=0), now.end.replace(second=59, microsecond=999999))) - self.assertEqual(now.full_hour(), DateSpan(now.start.replace(minute=0, second=0, microsecond=0), + self.assertEqual(now.full_hour, DateSpan(now.start.replace(minute=0, second=0, microsecond=0), 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), + 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))) - 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 - self.assertEqual(now.full_year(), + 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 + self.assertEqual(now.full_year, DateSpan(now.start.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0), now.end.replace(month=12, day=31, hour=23, minute=59, second=59, microsecond=999999))) diff --git a/tests/test_datespan_methods.py b/tests/test_datespan_methods.py index 6eef072..822ecc5 100644 --- a/tests/test_datespan_methods.py +++ b/tests/test_datespan_methods.py @@ -85,69 +85,69 @@ def test_with_year(self): self.assertEqual(result.end.year, 2024) def test_full_millisecond(self): - result = self.jan.full_millisecond() + result = self.jan.full_millisecond self.assertEqual(result.start.microsecond % 1000, 0) self.assertEqual(result.end.microsecond % 1000, 999) def test_full_second(self): - result = self.jan.full_second() + result = self.jan.full_second self.assertEqual(result.start.microsecond, 0) self.assertEqual(result.end.microsecond, 999999) def test_full_minute(self): - result = self.jan.full_minute() + result = self.jan.full_minute self.assertEqual(result.start.second, 0) self.assertEqual(result.end.second, 59) def test_full_hour(self): - result = self.jan.full_hour() + result = self.jan.full_hour self.assertEqual(result.start.minute, 0) self.assertEqual(result.end.minute, 59) def test_full_day(self): - result = self.jan.full_day() + result = self.jan.full_day self.assertEqual(result.start.hour, 0) self.assertEqual(result.end.hour, 23) def test_full_week(self): - result = self.jan.full_week() + result = self.jan.full_week self.assertEqual(result.start.weekday(), 0) self.assertEqual(result.end.weekday(), 6) def test_full_month(self): - result = self.jan.full_month() + result = self.jan.full_month self.assertEqual(result.start.day, 1) self.assertEqual(result.end.day, 31) def test_full_quarter(self): - result = self.jan.full_quarter() + result = self.jan.full_quarter self.assertEqual(result.start.month, 1) self.assertEqual(result.end.month, 3) def test_full_year(self): - result = self.jan.full_year() + result = self.jan.full_year self.assertEqual(result.start.month, 1) self.assertEqual(result.end.month, 12) def test_ytd(self): - result = self.jan.ytd() + result = self.jan.ytd self.assertEqual(result.start.month, 1) - self.assertEqual(result.end, self.today.end) + self.assertEqual(result.end, self.jan.end) def test_mtd(self): - result = self.jan.mtd() + result = self.jan.mtd self.assertEqual(result.start.day, 1) - self.assertEqual(result.end, self.today.end) + self.assertEqual(result.end, self.jan.end) def test_qtd(self): - result = self.jan.qtd() + result = self.jan.qtd self.assertEqual(result.start.day, 1) - self.assertEqual(result.end, self.today.end) + self.assertEqual(result.end, self.jan.end) def test_wtd(self): - result = self.jan.wtd() + result = self.jan.wtd self.assertEqual(result.start.weekday(), 0) - self.assertEqual(result.end, self.today.end) + self.assertEqual(result.end, self.jan.end) def test_shift(self): result = self.jan.shift(months=1) @@ -162,17 +162,17 @@ def test_shift_end(self): self.assertEqual(result.end, self.feb.end) def test_set_start(self): - new_start = DateSpan(datetime(2023, 1, 15)).full_day() + new_start = DateSpan(datetime(2023, 1, 15)).full_day result = self.jan.set_start(day=15) self.assertEqual(result.start, new_start.start) def test_set_end(self): - new_end = DateSpan(datetime(2023, 1, 15)).full_day() + new_end = DateSpan(datetime(2023, 1, 15)).full_day result = self.jan.set_end(day=15) self.assertEqual(result.end, new_end.end) def test_set(self): - new_date = DateSpan(datetime(2023, 1, 15)).full_day() + new_date = DateSpan(datetime(2023, 1, 15)).full_day result = self.jan.set(day=15) self.assertEqual(result.start, new_date.start) self.assertEqual(result.end, new_date.end) diff --git a/tests/test_datespanset.py b/tests/test_datespanset.py index 9cdf44f..792579e 100644 --- a/tests/test_datespanset.py +++ b/tests/test_datespanset.py @@ -69,21 +69,21 @@ def test_to_function(self): def test_datespans(self): - texts = [ # ("1st of January 2024", DateSpan(datetime(2024, 1, 1)).full_day()), + texts = [ # ("1st of January 2024", DateSpan(datetime(2024, 1, 1)).full_day), # ("1st day of January, February and March 2024", None), - ("2007-12-24T18:21Z", DateSpan(datetime(2007, 12, 24, 18, 21)).full_minute()), - ("next 3 days", DateSpan.now().shift(days=1).full_day().shift_end(days=2)), - ("last week", DateSpan.now().shift(days=-7).full_week()), + ("2007-12-24T18:21Z", DateSpan(datetime(2007, 12, 24, 18, 21)).full_minute), + ("next 3 days", DateSpan.now().shift(days=1).full_day.shift_end(days=2)), + ("last week", DateSpan.now().shift(days=-7).full_week), ("2010-01-01T12:00:00.001+02:00", DateSpan(datetime(2010, 1, 1, 12, 0, 0, 1000))), - ("2007-08-31T16:47+00:00", DateSpan(datetime(2007, 8, 31, 16, 47)).full_minute()), - ("2008-02-01T09:00:22+05", DateSpan(datetime(2008, 2, 1, 9, 0, 22)).full_second()), - ("2009-01-01T12:00:00+01:00", DateSpan(datetime(2009, 1, 1, 12, 0), None).full_second()), + ("2007-08-31T16:47+00:00", DateSpan(datetime(2007, 8, 31, 16, 47)).full_minute), + ("2008-02-01T09:00:22+05", DateSpan(datetime(2008, 2, 1, 9, 0, 22)).full_second), + ("2009-01-01T12:00:00+01:00", DateSpan(datetime(2009, 1, 1, 12, 0), None).full_second), # ("3rd week of 2024", None), - ("09.08.2024", DateSpan(datetime(2024, 9, 8)).full_day()), - ("2024/09/08", DateSpan(datetime(2024, 9, 8)).full_day()), - ("2024-09-08", DateSpan(datetime(2024, 9, 8)).full_day()), - ("19:00", DateSpan.now().with_time(time(19, 0)).full_minute()), - ("1:34:45", DateSpan.now().with_time(time(1, 34, 45)).full_second()), + ("09.08.2024", DateSpan(datetime(2024, 9, 8)).full_day), + ("2024/09/08", DateSpan(datetime(2024, 9, 8)).full_day), + ("2024-09-08", DateSpan(datetime(2024, 9, 8)).full_day), + ("19:00", DateSpan.now().with_time(time(19, 0)).full_minute), + ("1:34:45", DateSpan.now().with_time(time(1, 34, 45)).full_second), ("1:34:45.123", DateSpan.now().with_time(time(1, 34, 45, 123000))), ("1:34:45.123456", DateSpan.now().with_time(time(1, 34, 45, 123456))), @@ -204,24 +204,24 @@ def test_advanced(self): def test_parse_simple_datespans(self): texts = [ - ("last 3 month", DateSpan.now().shift(months=-1).full_month().shift_start(months=-2)), - ("2024", DateSpan(datetime(2024, 1, 1)).full_year()), + ("last 3 month", DateSpan.now().shift(months=-1).full_month.shift_start(months=-2)), + ("2024", DateSpan(datetime(2024, 1, 1)).full_year), ("past 3 month", DateSpan.now().shift_start(months=-3)), - ("previous 3 month", DateSpan.now().shift(months=-1).full_month().shift_start(months=-2)), - ("this quarter", DateSpan.now().full_quarter()), - ("this minute", DateSpan.now().full_minute()), - ("2024", DateSpan(datetime(2024, 1, 1)).full_year()), - ("March", DateSpan(datetime(datetime.now().year, 3, 1)).full_month()), - ("Jan 2024", DateSpan(datetime(2024, 1, 1)).full_month()), - ("last month", DateSpan(datetime.now()).full_month().shift(months=-1)), - ("previous month", DateSpan(datetime.now()).full_month().shift(months=-1)), - ("prev. month", DateSpan(datetime.now()).full_month().shift(months=-1)), - ("actual month", DateSpan(datetime.now()).full_month()), - ("next month", DateSpan(datetime.now()).full_month().shift(months=1)), - ("next year", DateSpan(datetime.now()).full_year().shift(years=1)), - ("today", DateSpan(datetime.now()).full_day()), - ("yesterday", DateSpan(datetime.now()).shift(days=-1).full_day()), - ("ytd", DateSpan(datetime.now()).ytd()), + ("previous 3 month", DateSpan.now().shift(months=-1).full_month.shift_start(months=-2)), + ("this quarter", DateSpan.now().full_quarter), + ("this minute", DateSpan.now().full_minute), + ("2024", DateSpan(datetime(2024, 1, 1)).full_year), + ("March", DateSpan(datetime(datetime.now().year, 3, 1)).full_month), + ("Jan 2024", DateSpan(datetime(2024, 1, 1)).full_month), + ("last month", DateSpan(datetime.now()).full_month.shift(months=-1)), + ("previous month", DateSpan(datetime.now()).full_month.shift(months=-1)), + ("prev. month", DateSpan(datetime.now()).full_month.shift(months=-1)), + ("actual month", DateSpan(datetime.now()).full_month), + ("next month", DateSpan(datetime.now()).full_month.shift(months=1)), + ("next year", DateSpan(datetime.now()).full_year.shift(years=1)), + ("today", DateSpan(datetime.now()).full_day), + ("yesterday", DateSpan(datetime.now()).shift(days=-1).full_day), + ("ytd", DateSpan(datetime.now()).ytd), ] for text, test in texts: if self.debug: