120 lines
4.0 KiB
Python
120 lines
4.0 KiB
Python
# 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 period specification parser.
|
|
|
|
"""
|
|
import calendar
|
|
import re
|
|
import typing as t
|
|
from datetime import date
|
|
|
|
from .period import Period
|
|
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
|
|
LastYear, Today, Yesterday, AllTime
|
|
|
|
DATE_SPEC_RE: str = r"(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?"
|
|
"""The regular expression of a date specification."""
|
|
|
|
|
|
def get_period(spec: str | None = None) -> Period:
|
|
"""Returns a period instance.
|
|
|
|
:param spec: The period specification, or omit for the default.
|
|
:return: The period instance.
|
|
:raise ValueError: When the period specification 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, end = __parse_spec(spec)
|
|
if start is not None and end is not None and start > end:
|
|
raise ValueError
|
|
return Period(start, end)
|
|
|
|
|
|
def __parse_spec(text: str) -> tuple[date | None, 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) -> 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 date(int(year), int(month), int(day))
|
|
if month is not None:
|
|
return date(int(year), int(month), 1)
|
|
return date(int(year), 1, 1)
|
|
|
|
|
|
def __get_end(year: str, month: str | None, day: str | None) -> 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 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 date(year_n, month_n, day_n)
|
|
return date(int(year), 12, 31)
|