Create a transformer for dates

pull/128/head
stijndcl 2022-08-28 23:33:52 +02:00
parent b581c3e5dc
commit e1af53cf44
3 changed files with 196 additions and 5 deletions

View File

@ -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')}."

View File

@ -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.")

View File

@ -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)