mirror of https://github.com/stijndcl/didier
Create a transformer for dates
parent
b581c3e5dc
commit
e1af53cf44
|
@ -1,4 +1,4 @@
|
||||||
from datetime import datetime
|
from datetime import date
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional, cast
|
from typing import Optional, cast
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ class Menu(EmbedPydantic):
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def to_embed(self, **kwargs) -> discord.Embed:
|
def to_embed(self, **kwargs) -> discord.Embed:
|
||||||
day_dt: datetime = cast(datetime, kwargs.get("day_dt"))
|
day_dt: date = cast(date, kwargs.get("day_dt"))
|
||||||
weekday = int_to_weekday(day_dt.weekday())
|
weekday = int_to_weekday(day_dt.weekday())
|
||||||
formatted_date = f"{leading('0', str(day_dt.day))}/{leading('0', str(day_dt.month))}/{day_dt.year}"
|
formatted_date = f"{leading('0', str(day_dt.day))}/{leading('0', str(day_dt.month))}/{day_dt.year}"
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ class Menu(EmbedPydantic):
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
|
|
||||||
def no_menu_found(day_dt: datetime) -> discord.Embed:
|
def no_menu_found(day_dt: date) -> discord.Embed:
|
||||||
"""Return a different embed if no menu could be found"""
|
"""Return a different embed if no menu could be found"""
|
||||||
embed = discord.Embed(title="Menu", colour=discord.Colour.red())
|
embed = discord.Embed(title="Menu", colour=discord.Colour.red())
|
||||||
embed.description = f"Unable to retrieve menu for {day_dt.strftime('%d/%m/%Y')}."
|
embed.description = f"Unable to retrieve menu for {day_dt.strftime('%d/%m/%Y')}."
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import contextlib
|
||||||
|
from datetime import date, timedelta
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from discord.ext.commands import ArgumentParsingError
|
||||||
|
|
||||||
|
from didier.utils.types.datetime import (
|
||||||
|
forward_to_next_weekday,
|
||||||
|
parse_dm_string,
|
||||||
|
str_to_weekday,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = ["date_converter"]
|
||||||
|
|
||||||
|
|
||||||
|
def date_converter(argument: Optional[str]) -> date:
|
||||||
|
"""Converter to turn a string into a date"""
|
||||||
|
# Store original argument for error message purposes
|
||||||
|
original_argument = argument
|
||||||
|
|
||||||
|
# Default to today
|
||||||
|
if not argument:
|
||||||
|
return date.today()
|
||||||
|
|
||||||
|
argument = argument.lower()
|
||||||
|
|
||||||
|
# Manual offsets
|
||||||
|
if argument in (
|
||||||
|
"tomorrow",
|
||||||
|
"tmrw",
|
||||||
|
"morgen",
|
||||||
|
):
|
||||||
|
return date.today() + timedelta(days=1)
|
||||||
|
|
||||||
|
if argument in ("overmorgen",):
|
||||||
|
return date.today() + timedelta(days=2)
|
||||||
|
|
||||||
|
# Weekdays passed in words
|
||||||
|
with contextlib.suppress(ValueError):
|
||||||
|
weekday = str_to_weekday(argument)
|
||||||
|
return forward_to_next_weekday(date.today(), weekday, allow_today=False)
|
||||||
|
|
||||||
|
# Date strings
|
||||||
|
with contextlib.suppress(ValueError):
|
||||||
|
return parse_dm_string(argument)
|
||||||
|
|
||||||
|
# Unparseable
|
||||||
|
raise ArgumentParsingError(f"Unable to interpret `{original_argument}` as a date.")
|
|
@ -1,18 +1,91 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
import re
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
|
from typing import TypeVar, Union
|
||||||
|
|
||||||
__all__ = ["LOCAL_TIMEZONE", "int_to_weekday", "str_to_date", "tz_aware_now"]
|
__all__ = [
|
||||||
|
"LOCAL_TIMEZONE",
|
||||||
|
"forward_to_next_weekday",
|
||||||
|
"int_to_weekday",
|
||||||
|
"parse_dm_string",
|
||||||
|
"str_to_date",
|
||||||
|
"str_to_month",
|
||||||
|
"str_to_weekday",
|
||||||
|
"tz_aware_now",
|
||||||
|
]
|
||||||
|
|
||||||
from typing import Union
|
DateType = TypeVar("DateType", datetime.date, datetime.datetime)
|
||||||
|
|
||||||
LOCAL_TIMEZONE = zoneinfo.ZoneInfo("Europe/Brussels")
|
LOCAL_TIMEZONE = zoneinfo.ZoneInfo("Europe/Brussels")
|
||||||
|
|
||||||
|
|
||||||
|
def forward_to_next_weekday(day_dt: DateType, target_weekday: int, *, allow_today: bool = False) -> DateType:
|
||||||
|
"""Forward a date to the next occurence of a weekday"""
|
||||||
|
if not 0 <= target_weekday <= 6:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
# Skip at least one day
|
||||||
|
if not allow_today:
|
||||||
|
day_dt += datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
while day_dt.weekday() != target_weekday:
|
||||||
|
day_dt += datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
return day_dt
|
||||||
|
|
||||||
|
|
||||||
def int_to_weekday(number: int) -> str: # pragma: no cover # it's useless to write a test for this
|
def int_to_weekday(number: int) -> str: # pragma: no cover # it's useless to write a test for this
|
||||||
"""Get the Dutch name of a weekday from the number"""
|
"""Get the Dutch name of a weekday from the number"""
|
||||||
return ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"][number]
|
return ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"][number]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_dm_string(argument: str) -> datetime.date:
|
||||||
|
"""Parse a string to [day]/[month]
|
||||||
|
|
||||||
|
The year is set to the current year by default, as this can be changed easily.
|
||||||
|
|
||||||
|
This supports:
|
||||||
|
- DD/MM
|
||||||
|
- DD (month defaults to current)
|
||||||
|
- DD [Dutch Month, possibly abbreviated]
|
||||||
|
- DD [English Month, possibly abbreviated]
|
||||||
|
- [Dutch Month, possibly abbreviated] DD
|
||||||
|
- [English Month, possibly abbreviated] DD
|
||||||
|
"""
|
||||||
|
argument = argument.lower()
|
||||||
|
today = datetime.date.today()
|
||||||
|
|
||||||
|
# DD/MM
|
||||||
|
if "/" in argument:
|
||||||
|
spl = argument.split("/")
|
||||||
|
if len(spl) != 2:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
return datetime.date(day=int(spl[0]), month=int(spl[1]), year=today.year)
|
||||||
|
|
||||||
|
# Try to interpret text
|
||||||
|
spl = argument.split(" ")
|
||||||
|
if len(spl) != 2:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
# Day Month
|
||||||
|
match = re.search(r"\d+", spl[0]).group()
|
||||||
|
if match is not None:
|
||||||
|
day = int(match)
|
||||||
|
month = str_to_month(spl[1])
|
||||||
|
return datetime.date(day=day, month=month, year=today.year)
|
||||||
|
|
||||||
|
# Month Day
|
||||||
|
match = re.search(r"\d+", spl[0]).group()
|
||||||
|
if match is not None:
|
||||||
|
day = int(match)
|
||||||
|
month = str_to_month(spl[0])
|
||||||
|
return datetime.date(day=day, month=month, year=today.year)
|
||||||
|
|
||||||
|
# Unparseable
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
def str_to_date(date_str: str, formats: Union[list[str], str] = "%d/%m/%Y") -> datetime.date:
|
def str_to_date(date_str: str, formats: Union[list[str], str] = "%d/%m/%Y") -> datetime.date:
|
||||||
"""Turn a string into a DD/MM/YYYY date"""
|
"""Turn a string into a DD/MM/YYYY date"""
|
||||||
# Allow passing multiple formats in a list
|
# Allow passing multiple formats in a list
|
||||||
|
@ -28,6 +101,76 @@ def str_to_date(date_str: str, formats: Union[list[str], str] = "%d/%m/%Y") -> d
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
|
def str_to_month(argument: str) -> int:
|
||||||
|
"""Turn a string int oa month, bilingual"""
|
||||||
|
argument = argument.lower()
|
||||||
|
|
||||||
|
month_dict = {
|
||||||
|
# English
|
||||||
|
"january": 1,
|
||||||
|
"february": 2,
|
||||||
|
"march": 3,
|
||||||
|
"april": 4,
|
||||||
|
"may": 5,
|
||||||
|
"june": 6,
|
||||||
|
"july": 7,
|
||||||
|
# August is a prefix of Augustus so it is skipped
|
||||||
|
"september": 9,
|
||||||
|
"october": 10,
|
||||||
|
"november": 11,
|
||||||
|
"december": 12,
|
||||||
|
# Dutch
|
||||||
|
"januari": 1,
|
||||||
|
"februari": 2,
|
||||||
|
"maart": 3,
|
||||||
|
# April is the same in English so it is skipped
|
||||||
|
"mei": 5,
|
||||||
|
"juni": 6,
|
||||||
|
"juli": 7,
|
||||||
|
"augustus": 8,
|
||||||
|
# September is the same in English so it is skipped
|
||||||
|
"oktober": 10,
|
||||||
|
# November is the same in English so it is skipped
|
||||||
|
# December is the same in English so it is skipped
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in month_dict.items():
|
||||||
|
if key.startswith(argument):
|
||||||
|
return value
|
||||||
|
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
|
def str_to_weekday(argument: str) -> int:
|
||||||
|
"""Turn a string into a weekday, bilingual"""
|
||||||
|
argument = argument.lower()
|
||||||
|
|
||||||
|
weekday_dict = {
|
||||||
|
# English
|
||||||
|
"monday": 0,
|
||||||
|
"tuesday": 1,
|
||||||
|
"wednesday": 2,
|
||||||
|
"thursday": 3,
|
||||||
|
"friday": 4,
|
||||||
|
"saturday": 5,
|
||||||
|
"sunday": 6,
|
||||||
|
# Dutch
|
||||||
|
"maandag": 0,
|
||||||
|
"dinsdag": 1,
|
||||||
|
"woensdag": 2,
|
||||||
|
"donderdag": 3,
|
||||||
|
"vrijdag": 4,
|
||||||
|
"zaterdag": 5,
|
||||||
|
"zondag": 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in weekday_dict.items():
|
||||||
|
if key.startswith(argument):
|
||||||
|
return value
|
||||||
|
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
def tz_aware_now() -> datetime.datetime:
|
def tz_aware_now() -> datetime.datetime:
|
||||||
"""Get the current date & time, but timezone-aware"""
|
"""Get the current date & time, but timezone-aware"""
|
||||||
return datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).astimezone(LOCAL_TIMEZONE)
|
return datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).astimezone(LOCAL_TIMEZONE)
|
||||||
|
|
Loading…
Reference in New Issue