|
|
|
@ -0,0 +1,643 @@
|
|
|
|
|
# The Mia! Accounting Flask Project.
|
|
|
|
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
|
|
|
|
|
|
|
|
|
# Copyright (c) 2023 imacat.
|
|
|
|
|
#
|
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
|
#
|
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
#
|
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
|
# limitations under the License.
|
|
|
|
|
"""The date period.
|
|
|
|
|
|
|
|
|
|
This file is largely taken from the NanoParma ERP project, first written in
|
|
|
|
|
2021/9/16 by imacat (imacat@nanoparma.com).
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
import calendar
|
|
|
|
|
import datetime
|
|
|
|
|
import re
|
|
|
|
|
import typing as t
|
|
|
|
|
|
|
|
|
|
from accounting.locale import gettext
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Period:
|
|
|
|
|
"""A date period."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, start: datetime.date | None, end: datetime.date | None):
|
|
|
|
|
"""Constructs a new date period.
|
|
|
|
|
|
|
|
|
|
:param start: The start date, or None from the very beginning.
|
|
|
|
|
:param end: The end date, or None till no end.
|
|
|
|
|
"""
|
|
|
|
|
self.start: datetime.date | None = start
|
|
|
|
|
"""The start of the period."""
|
|
|
|
|
self.end: datetime.date | None = end
|
|
|
|
|
"""The end of the period."""
|
|
|
|
|
self.is_default: bool = False
|
|
|
|
|
"""Whether the is the default period."""
|
|
|
|
|
self.is_this_month: bool = False
|
|
|
|
|
"""Whether the period is this month."""
|
|
|
|
|
self.is_last_month: bool = False
|
|
|
|
|
"""Whether the period is last month."""
|
|
|
|
|
self.is_since_last_month: bool = False
|
|
|
|
|
"""Whether the period is since last month."""
|
|
|
|
|
self.is_this_year: bool = False
|
|
|
|
|
"""Whether the period is this year."""
|
|
|
|
|
self.is_last_year: bool = False
|
|
|
|
|
"""Whether the period is last year."""
|
|
|
|
|
self.is_today: bool = False
|
|
|
|
|
"""Whether the period is today."""
|
|
|
|
|
self.is_yesterday: bool = False
|
|
|
|
|
"""Whether the period is yesterday."""
|
|
|
|
|
self.is_all: bool = start is None and end is None
|
|
|
|
|
"""Whether the period is all time."""
|
|
|
|
|
self.spec: str = ""
|
|
|
|
|
"""The period specification."""
|
|
|
|
|
self.desc: str = ""
|
|
|
|
|
"""The text description."""
|
|
|
|
|
self.is_a_month: bool = False
|
|
|
|
|
"""Whether the period is a whole month."""
|
|
|
|
|
self.is_type_month: bool = False
|
|
|
|
|
"""Whether the period is for the month chooser."""
|
|
|
|
|
self.is_a_year: bool = False
|
|
|
|
|
"""Whether the period is a whole year."""
|
|
|
|
|
self.is_a_day: bool = False
|
|
|
|
|
"""Whether the period is a single day."""
|
|
|
|
|
self._set_properties()
|
|
|
|
|
|
|
|
|
|
def _set_properties(self) -> None:
|
|
|
|
|
"""Sets the following properties.
|
|
|
|
|
|
|
|
|
|
* self.spec
|
|
|
|
|
* self.desc
|
|
|
|
|
* self.is_a_month
|
|
|
|
|
* self.is_type_month
|
|
|
|
|
* self.is_a_year
|
|
|
|
|
* self.is_a_day
|
|
|
|
|
|
|
|
|
|
Override this method to set the properties in the subclasses, to skip
|
|
|
|
|
the calculation.
|
|
|
|
|
|
|
|
|
|
:return: None.
|
|
|
|
|
"""
|
|
|
|
|
self.spec = PeriodSpecification(self).spec
|
|
|
|
|
self.desc = PeriodDescription(self).desc
|
|
|
|
|
if self.start is None or self.end is None:
|
|
|
|
|
return
|
|
|
|
|
self.is_a_month = self.start.day == 1 \
|
|
|
|
|
and self.end == _month_end(self.start)
|
|
|
|
|
self.is_type_month = self.is_a_month
|
|
|
|
|
self.is_a_year = self.start == datetime.date(self.start.year, 1, 1) \
|
|
|
|
|
and self.end == datetime.date(self.start.year, 12, 31)
|
|
|
|
|
self.is_a_day = self.start == self.end
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def get_instance(cls, spec: str | None = None) -> t.Self:
|
|
|
|
|
"""Returns a period instance.
|
|
|
|
|
|
|
|
|
|
:param spec: The period specification, or omit for the default.
|
|
|
|
|
:return: The period instance.
|
|
|
|
|
:raise ValueError: When the period is invalid.
|
|
|
|
|
"""
|
|
|
|
|
if spec is None:
|
|
|
|
|
return ThisMonth()
|
|
|
|
|
named_periods: dict[str, t.Type[t.Callable[[], Period]]] = {
|
|
|
|
|
"this-month": lambda: ThisMonth(),
|
|
|
|
|
"last-month": lambda: LastMonth(),
|
|
|
|
|
"since-last-month": lambda: SinceLastMonth(),
|
|
|
|
|
"this-year": lambda: ThisYear(),
|
|
|
|
|
"last-year": lambda: LastYear(),
|
|
|
|
|
"today": lambda: Today(),
|
|
|
|
|
"yesterday": lambda: Yesterday(),
|
|
|
|
|
"all-time": lambda: AllTime(),
|
|
|
|
|
}
|
|
|
|
|
if spec in named_periods:
|
|
|
|
|
return named_periods[spec]()
|
|
|
|
|
start: datetime.date
|
|
|
|
|
end: datetime.date
|
|
|
|
|
start, end = _parse_period_spec(spec)
|
|
|
|
|
if start is not None and end is not None and start > end:
|
|
|
|
|
raise ValueError
|
|
|
|
|
return cls(start, end)
|
|
|
|
|
|
|
|
|
|
def is_year(self, year: int) -> bool:
|
|
|
|
|
"""Returns whether the period is the specific year period.
|
|
|
|
|
|
|
|
|
|
:param year: The year.
|
|
|
|
|
:return: True if the period is the year period, or False otherwise.
|
|
|
|
|
"""
|
|
|
|
|
if not self.is_a_year:
|
|
|
|
|
return False
|
|
|
|
|
return self.start.year == year
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def is_type_arbitrary(self) -> bool:
|
|
|
|
|
"""Returns whether this period is an arbitrary period.
|
|
|
|
|
|
|
|
|
|
:return: True if this is an arbitrary period, or False otherwise.
|
|
|
|
|
"""
|
|
|
|
|
return not self.is_type_month and not self.is_a_year \
|
|
|
|
|
and not self.is_a_day
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def before(self) -> t.Self | None:
|
|
|
|
|
"""Returns the period before this period.
|
|
|
|
|
|
|
|
|
|
:return: The period before this period.
|
|
|
|
|
"""
|
|
|
|
|
if self.start is None:
|
|
|
|
|
return None
|
|
|
|
|
return Period(None, self.start - datetime.timedelta(days=1))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PeriodSpecification:
|
|
|
|
|
"""The period specification composer."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, period: Period):
|
|
|
|
|
"""Constructs the period specification composer.
|
|
|
|
|
|
|
|
|
|
:param period: The period.
|
|
|
|
|
"""
|
|
|
|
|
self.__start: datetime.date = period.start
|
|
|
|
|
self.__end: datetime.date = period.end
|
|
|
|
|
self.spec: str = self.__get_spec()
|
|
|
|
|
|
|
|
|
|
def __get_spec(self) -> str:
|
|
|
|
|
"""Returns the period specification.
|
|
|
|
|
|
|
|
|
|
:return: The period specification.
|
|
|
|
|
"""
|
|
|
|
|
if self.__start is None and self.__end is None:
|
|
|
|
|
return "-"
|
|
|
|
|
if self.__end is None:
|
|
|
|
|
return self.__get_since_spec()
|
|
|
|
|
if self.__start is None:
|
|
|
|
|
return self.__get_until_spec()
|
|
|
|
|
try:
|
|
|
|
|
return self.__get_year_spec()
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
try:
|
|
|
|
|
return self.__get_month_spec()
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
return self.__get_day_spec()
|
|
|
|
|
|
|
|
|
|
def __get_since_spec(self) -> str:
|
|
|
|
|
"""Returns the period specification without the end day.
|
|
|
|
|
|
|
|
|
|
:return: The period specification without the end day
|
|
|
|
|
"""
|
|
|
|
|
if self.__start.month == 1 and self.__start.day == 1:
|
|
|
|
|
return self.__start.strftime("%Y-")
|
|
|
|
|
if self.__start.day == 1:
|
|
|
|
|
return self.__start.strftime("%Y-%m-")
|
|
|
|
|
return self.__start.strftime("%Y-%m-%d-")
|
|
|
|
|
|
|
|
|
|
def __get_until_spec(self) -> str:
|
|
|
|
|
"""Returns the period specification without the start day.
|
|
|
|
|
|
|
|
|
|
:return: The period specification without the start day
|
|
|
|
|
"""
|
|
|
|
|
if self.__end.month == 12 and self.__end.day == 31:
|
|
|
|
|
return self.__end.strftime("-%Y")
|
|
|
|
|
if (self.__end + datetime.timedelta(days=1)).day == 1:
|
|
|
|
|
return self.__end.strftime("-%Y-%m")
|
|
|
|
|
return self.__end.strftime("-%Y-%m-%d")
|
|
|
|
|
|
|
|
|
|
def __get_year_spec(self) -> str:
|
|
|
|
|
"""Returns the period specification as a year range.
|
|
|
|
|
|
|
|
|
|
:return: The period specification as a year range.
|
|
|
|
|
:raise ValueError: The period is not a year range.
|
|
|
|
|
"""
|
|
|
|
|
if self.__start.month != 1 or self.__start.day != 1 \
|
|
|
|
|
or self.__end.month != 12 or self.__end.day != 31:
|
|
|
|
|
raise ValueError
|
|
|
|
|
if self.__start.year == self.__end.year:
|
|
|
|
|
return "%04d" % self.__start.year
|
|
|
|
|
return "%04d-%04d" % (self.__start.year, self.__end.year)
|
|
|
|
|
|
|
|
|
|
def __get_month_spec(self) -> str:
|
|
|
|
|
"""Returns the period specification as a month range.
|
|
|
|
|
|
|
|
|
|
:return: The period specification as a month range.
|
|
|
|
|
:raise ValueError: The period is not a month range.
|
|
|
|
|
"""
|
|
|
|
|
if self.__start.day != 1 or self.__end != _month_end(self.__end):
|
|
|
|
|
raise ValueError
|
|
|
|
|
if self.__start.year == self.__end.year \
|
|
|
|
|
and self.__start.month == self.__end.month:
|
|
|
|
|
return "%04d-%02d" % (self.__start.year, self.__start.month)
|
|
|
|
|
return "%04d-%02d-%04d-%02d" % (
|
|
|
|
|
self.__start.year, self.__start.month,
|
|
|
|
|
self.__end.year, self.__end.month)
|
|
|
|
|
|
|
|
|
|
def __get_day_spec(self) -> str:
|
|
|
|
|
"""Returns the period specification as a day range.
|
|
|
|
|
|
|
|
|
|
:return: The period specification as a day range.
|
|
|
|
|
:raise ValueError: The period is a month or year range.
|
|
|
|
|
"""
|
|
|
|
|
if self.__start == self.__end:
|
|
|
|
|
return "%04d-%02d-%02d" % (
|
|
|
|
|
self.__start.year, self.__start.month, self.__start.day)
|
|
|
|
|
return "%04d-%02d-%02d-%04d-%02d-%02d" % (
|
|
|
|
|
self.__start.year, self.__start.month, self.__start.day,
|
|
|
|
|
self.__end.year, self.__end.month, self.__end.day)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PeriodDescription:
|
|
|
|
|
"""The period description composer."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, period: Period):
|
|
|
|
|
"""Constructs the period description composer.
|
|
|
|
|
|
|
|
|
|
:param period: The period.
|
|
|
|
|
"""
|
|
|
|
|
self.__start: datetime.date = period.start
|
|
|
|
|
self.__end: datetime.date = period.end
|
|
|
|
|
self.desc: str = self.__get_desc()
|
|
|
|
|
|
|
|
|
|
def __get_desc(self) -> str:
|
|
|
|
|
"""Returns the period description.
|
|
|
|
|
|
|
|
|
|
:return: The period description.
|
|
|
|
|
"""
|
|
|
|
|
if self.__start is None and self.__end is None:
|
|
|
|
|
return gettext("for all time")
|
|
|
|
|
if self.__start is None:
|
|
|
|
|
return self.__get_until_desc()
|
|
|
|
|
if self.__end is None:
|
|
|
|
|
return self.__get_since_desc()
|
|
|
|
|
try:
|
|
|
|
|
return self.__get_year_desc()
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
try:
|
|
|
|
|
return self.__get_month_desc()
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
return self.__get_day_desc()
|
|
|
|
|
|
|
|
|
|
def __get_since_desc(self) -> str:
|
|
|
|
|
"""Returns the description without the end day.
|
|
|
|
|
|
|
|
|
|
:return: The description without the end day.
|
|
|
|
|
"""
|
|
|
|
|
def get_start_desc() -> str:
|
|
|
|
|
"""Returns the description of the start day.
|
|
|
|
|
|
|
|
|
|
:return: The description of the start day.
|
|
|
|
|
"""
|
|
|
|
|
if self.__start.month == 1 and self.__start.day == 1:
|
|
|
|
|
return str(self.__start.year)
|
|
|
|
|
if self.__start.day == 1:
|
|
|
|
|
return self.__format_month(self.__start)
|
|
|
|
|
return self.__format_date(self.__start)
|
|
|
|
|
|
|
|
|
|
return gettext("since %(start)s", start=get_start_desc())
|
|
|
|
|
|
|
|
|
|
def __get_until_desc(self) -> str:
|
|
|
|
|
"""Returns the description without the start day.
|
|
|
|
|
|
|
|
|
|
:return: The description without the start day.
|
|
|
|
|
"""
|
|
|
|
|
def get_end_desc() -> str:
|
|
|
|
|
"""Returns the description of the end day.
|
|
|
|
|
|
|
|
|
|
:return: The description of the end day.
|
|
|
|
|
"""
|
|
|
|
|
if self.__end.month == 12 and self.__end.day == 31:
|
|
|
|
|
return str(self.__end.year)
|
|
|
|
|
if (self.__end + datetime.timedelta(days=1)).day == 1:
|
|
|
|
|
return self.__format_month(self.__end)
|
|
|
|
|
return self.__format_date(self.__end)
|
|
|
|
|
|
|
|
|
|
return gettext("until %(end)s", end=get_end_desc())
|
|
|
|
|
|
|
|
|
|
def __get_year_desc(self) -> str:
|
|
|
|
|
"""Returns the description as a year range.
|
|
|
|
|
|
|
|
|
|
:return: The description as a year range.
|
|
|
|
|
:raise ValueError: The period is not a year range.
|
|
|
|
|
"""
|
|
|
|
|
if self.__start.month != 1 or self.__start.day != 1 \
|
|
|
|
|
or self.__end.month != 12 or self.__end.day != 31:
|
|
|
|
|
raise ValueError
|
|
|
|
|
start: str = str(self.__start.year)
|
|
|
|
|
if self.__start.year == self.__end.year:
|
|
|
|
|
return self.__get_in_desc(start)
|
|
|
|
|
return self.__get_from_to_desc(start, str(self.__end.year))
|
|
|
|
|
|
|
|
|
|
def __get_month_desc(self) -> str:
|
|
|
|
|
"""Returns the description as a month range.
|
|
|
|
|
|
|
|
|
|
:return: The description as a month range.
|
|
|
|
|
:raise ValueError: The period is not a month range.
|
|
|
|
|
"""
|
|
|
|
|
if self.__start.day != 1 or self.__end != _month_end(self.__end):
|
|
|
|
|
raise ValueError
|
|
|
|
|
start: str = self.__format_month(self.__start)
|
|
|
|
|
if self.__start.year == self.__end.year \
|
|
|
|
|
and self.__start.month == self.__end.month:
|
|
|
|
|
return self.__get_in_desc(start)
|
|
|
|
|
if self.__start.year == self.__end.year:
|
|
|
|
|
return self.__get_from_to_desc(start, str(self.__end.month))
|
|
|
|
|
return self.__get_from_to_desc(start, self.__format_month(self.__end))
|
|
|
|
|
|
|
|
|
|
def __get_day_desc(self) -> str:
|
|
|
|
|
"""Returns the description as a day range.
|
|
|
|
|
|
|
|
|
|
:return: The description as a day range.
|
|
|
|
|
:raise ValueError: The period is a month or year range.
|
|
|
|
|
"""
|
|
|
|
|
start: str = self.__format_day(self.__start)
|
|
|
|
|
if self.__start == self.__end:
|
|
|
|
|
return self.__get_in_desc(start)
|
|
|
|
|
if self.__start.year == self.__end.year \
|
|
|
|
|
and self.__start.month == self.__end.month:
|
|
|
|
|
return self.__get_from_to_desc(start, str(self.__end.day))
|
|
|
|
|
if self.__start.year == self.__end.year:
|
|
|
|
|
end_month_day: str = f"{self.__end.month}/{self.__end.day}"
|
|
|
|
|
return self.__get_from_to_desc(start, end_month_day)
|
|
|
|
|
return self.__get_from_to_desc(start, self.__format_day(self.__end))
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def __format_date(date: datetime.date) -> str:
|
|
|
|
|
"""Formats a date.
|
|
|
|
|
|
|
|
|
|
:param date: The date.
|
|
|
|
|
:return: The formatted date.
|
|
|
|
|
"""
|
|
|
|
|
return f"{date.year}/{date.month}/{date.day}"
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def __format_month(month: datetime.date) -> str:
|
|
|
|
|
"""Formats a month.
|
|
|
|
|
|
|
|
|
|
:param month: The month.
|
|
|
|
|
:return: The formatted month.
|
|
|
|
|
"""
|
|
|
|
|
return f"{month.year}/{month.month}"
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def __format_day(day: datetime.date) -> str:
|
|
|
|
|
"""Formats a day.
|
|
|
|
|
|
|
|
|
|
:param day: The day.
|
|
|
|
|
:return: The formatted day.
|
|
|
|
|
"""
|
|
|
|
|
return f"{day.year}/{day.month}/{day.day}"
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def __get_in_desc(period: str) -> str:
|
|
|
|
|
"""Returns the description of a whole year, month, or day.
|
|
|
|
|
|
|
|
|
|
:param period: The time period.
|
|
|
|
|
:return: The description of a whole year, month, or day.
|
|
|
|
|
"""
|
|
|
|
|
return gettext("in %(period)s", period=period)
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def __get_from_to_desc(start: str, end: str) -> str:
|
|
|
|
|
"""Returns the description of a separated start and end.
|
|
|
|
|
|
|
|
|
|
:param start: The start.
|
|
|
|
|
:param end: The end.
|
|
|
|
|
:return: The description of the separated start and end.
|
|
|
|
|
"""
|
|
|
|
|
return gettext("in %(start)s-%(end)s", start=start, end=end)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ThisMonth(Period):
|
|
|
|
|
"""The period of this month."""
|
|
|
|
|
def __init__(self):
|
|
|
|
|
today: datetime.date = datetime.date.today()
|
|
|
|
|
this_month_start: datetime.date \
|
|
|
|
|
= datetime.date(today.year, today.month, 1)
|
|
|
|
|
super().__init__(this_month_start, _month_end(today))
|
|
|
|
|
self.is_default = True
|
|
|
|
|
self.is_this_month = True
|
|
|
|
|
|
|
|
|
|
def _set_properties(self) -> None:
|
|
|
|
|
self.spec = "this-month"
|
|
|
|
|
self.desc = gettext("This month")
|
|
|
|
|
self.is_a_month = True
|
|
|
|
|
self.is_type_month = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LastMonth(Period):
|
|
|
|
|
"""The period of this month."""
|
|
|
|
|
def __init__(self):
|
|
|
|
|
today: datetime.date = datetime.date.today()
|
|
|
|
|
year: int = today.year
|
|
|
|
|
month: int = today.month - 1
|
|
|
|
|
if month < 1:
|
|
|
|
|
year = year - 1
|
|
|
|
|
month = 12
|
|
|
|
|
start: datetime.date = datetime.date(year, month, 1)
|
|
|
|
|
super().__init__(start, _month_end(start))
|
|
|
|
|
self.is_last_month = True
|
|
|
|
|
|
|
|
|
|
def _set_properties(self) -> None:
|
|
|
|
|
self.spec = "last-month"
|
|
|
|
|
self.desc = gettext("Last month")
|
|
|
|
|
self.is_a_month = True
|
|
|
|
|
self.is_type_month = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SinceLastMonth(Period):
|
|
|
|
|
"""The period of this month."""
|
|
|
|
|
def __init__(self):
|
|
|
|
|
today: datetime.date = datetime.date.today()
|
|
|
|
|
year: int = today.year
|
|
|
|
|
month: int = today.month - 1
|
|
|
|
|
if month < 1:
|
|
|
|
|
year = year - 1
|
|
|
|
|
month = 12
|
|
|
|
|
start: datetime.date = datetime.date(year, month, 1)
|
|
|
|
|
super().__init__(start, None)
|
|
|
|
|
self.is_since_last_month = True
|
|
|
|
|
|
|
|
|
|
def _set_properties(self) -> None:
|
|
|
|
|
self.spec = "since-last-month"
|
|
|
|
|
self.desc = gettext("Since last month")
|
|
|
|
|
self.is_type_month = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ThisYear(Period):
|
|
|
|
|
"""The period of this year."""
|
|
|
|
|
def __init__(self):
|
|
|
|
|
year: int = datetime.date.today().year
|
|
|
|
|
start: datetime.date = datetime.date(year, 1, 1)
|
|
|
|
|
end: datetime.date = datetime.date(year, 12, 31)
|
|
|
|
|
super().__init__(start, end)
|
|
|
|
|
self.is_this_year = True
|
|
|
|
|
|
|
|
|
|
def _set_properties(self) -> None:
|
|
|
|
|
self.spec = "this-year"
|
|
|
|
|
self.desc = gettext("This year")
|
|
|
|
|
self.is_a_year = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LastYear(Period):
|
|
|
|
|
"""The period of last year."""
|
|
|
|
|
def __init__(self):
|
|
|
|
|
year: int = datetime.date.today().year
|
|
|
|
|
start: datetime.date = datetime.date(year - 1, 1, 1)
|
|
|
|
|
end: datetime.date = datetime.date(year - 1, 12, 31)
|
|
|
|
|
super().__init__(start, end)
|
|
|
|
|
self.is_last_year = True
|
|
|
|
|
|
|
|
|
|
def _set_properties(self) -> None:
|
|
|
|
|
self.spec = "last-year"
|
|
|
|
|
self.desc = gettext("Last year")
|
|
|
|
|
self.is_a_year = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Today(Period):
|
|
|
|
|
"""The period of today."""
|
|
|
|
|
def __init__(self):
|
|
|
|
|
today: datetime.date = datetime.date.today()
|
|
|
|
|
super().__init__(today, today)
|
|
|
|
|
self.is_today = True
|
|
|
|
|
|
|
|
|
|
def _set_properties(self) -> None:
|
|
|
|
|
self.spec = "today"
|
|
|
|
|
self.desc = gettext("Today")
|
|
|
|
|
self.is_a_day = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Yesterday(Period):
|
|
|
|
|
"""The period of yesterday."""
|
|
|
|
|
def __init__(self):
|
|
|
|
|
yesterday: datetime.date \
|
|
|
|
|
= datetime.date.today() - datetime.timedelta(days=1)
|
|
|
|
|
super().__init__(yesterday, yesterday)
|
|
|
|
|
self.is_yesterday = True
|
|
|
|
|
|
|
|
|
|
def _set_properties(self) -> None:
|
|
|
|
|
self.spec = "yesterday"
|
|
|
|
|
self.desc = gettext("Yesterday")
|
|
|
|
|
self.is_a_day = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AllTime(Period):
|
|
|
|
|
"""The period of all time."""
|
|
|
|
|
def __init__(self):
|
|
|
|
|
super().__init__(None, None)
|
|
|
|
|
self.is_all = True
|
|
|
|
|
|
|
|
|
|
def _set_properties(self) -> None:
|
|
|
|
|
self.spec = "all-time"
|
|
|
|
|
self.desc = gettext("All")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TemplatePeriod(Period):
|
|
|
|
|
"""The period template."""
|
|
|
|
|
def __init__(self):
|
|
|
|
|
super().__init__(None, None)
|
|
|
|
|
|
|
|
|
|
def _set_properties(self) -> None:
|
|
|
|
|
self.spec = "PERIOD"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class YearPeriod(Period):
|
|
|
|
|
"""A year period."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, year: int):
|
|
|
|
|
"""Constructs a year period.
|
|
|
|
|
|
|
|
|
|
:param year: The year.
|
|
|
|
|
"""
|
|
|
|
|
start: datetime.date = datetime.date(year, 1, 1)
|
|
|
|
|
end: datetime.date = datetime.date(year, 12, 31)
|
|
|
|
|
super().__init__(start, end)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DATE_SPEC_RE: str = r"(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?"
|
|
|
|
|
"""The regular expression of a date specification."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_period_spec(text: str) \
|
|
|
|
|
-> tuple[datetime.date | None, datetime.date | None]:
|
|
|
|
|
"""Parses the period specification.
|
|
|
|
|
|
|
|
|
|
:param text: The period specification.
|
|
|
|
|
:return: The start and end day of the period. The start and end day
|
|
|
|
|
may be None.
|
|
|
|
|
:raise ValueError: When the date is invalid.
|
|
|
|
|
"""
|
|
|
|
|
if text == "-":
|
|
|
|
|
return None, None
|
|
|
|
|
m = re.match(f"^{DATE_SPEC_RE}$", text)
|
|
|
|
|
if m is not None:
|
|
|
|
|
return __get_start(m[1], m[2], m[3]), \
|
|
|
|
|
__get_end(m[1], m[2], m[3])
|
|
|
|
|
m = re.match(f"^{DATE_SPEC_RE}-$", text)
|
|
|
|
|
if m is not None:
|
|
|
|
|
return __get_start(m[1], m[2], m[3]), None
|
|
|
|
|
m = re.match(f"-{DATE_SPEC_RE}$", text)
|
|
|
|
|
if m is not None:
|
|
|
|
|
return None, __get_end(m[1], m[2], m[3])
|
|
|
|
|
m = re.match(f"^{DATE_SPEC_RE}-{DATE_SPEC_RE}$", text)
|
|
|
|
|
if m is not None:
|
|
|
|
|
return __get_start(m[1], m[2], m[3]), \
|
|
|
|
|
__get_end(m[4], m[5], m[6])
|
|
|
|
|
raise ValueError
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __get_start(year: str, month: str | None, day: str | None)\
|
|
|
|
|
-> datetime.date:
|
|
|
|
|
"""Returns the start of the period from the date representation.
|
|
|
|
|
|
|
|
|
|
:param year: The year.
|
|
|
|
|
:param month: The month, if any.
|
|
|
|
|
:param day: The day, if any.
|
|
|
|
|
:return: The start of the period.
|
|
|
|
|
:raise ValueError: When the date is invalid.
|
|
|
|
|
"""
|
|
|
|
|
if day is not None:
|
|
|
|
|
return datetime.date(int(year), int(month), int(day))
|
|
|
|
|
if month is not None:
|
|
|
|
|
return datetime.date(int(year), int(month), 1)
|
|
|
|
|
return datetime.date(int(year), 1, 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __get_end(year: str, month: str | None, day: str | None)\
|
|
|
|
|
-> datetime.date:
|
|
|
|
|
"""Returns the end of the period from the date representation.
|
|
|
|
|
|
|
|
|
|
:param year: The year.
|
|
|
|
|
:param month: The month, if any.
|
|
|
|
|
:param day: The day, if any.
|
|
|
|
|
:return: The end of the period.
|
|
|
|
|
:raise ValueError: When the date is invalid.
|
|
|
|
|
"""
|
|
|
|
|
if day is not None:
|
|
|
|
|
return datetime.date(int(year), int(month), int(day))
|
|
|
|
|
if month is not None:
|
|
|
|
|
year_n: int = int(year)
|
|
|
|
|
month_n: int = int(month)
|
|
|
|
|
day_n: int = calendar.monthrange(year_n, month_n)[1]
|
|
|
|
|
return datetime.date(year_n, month_n, day_n)
|
|
|
|
|
return datetime.date(int(year), 12, 31)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _month_end(date: datetime.date) -> datetime.date:
|
|
|
|
|
"""Returns the end day of month for a date.
|
|
|
|
|
|
|
|
|
|
:param date: The date.
|
|
|
|
|
:return: The end day of the month of that day.
|
|
|
|
|
"""
|
|
|
|
|
day: int = calendar.monthrange(date.year, date.month)[1]
|
|
|
|
|
return datetime.date(date.year, date.month, day)
|