mirror of https://github.com/stijndcl/didier
205 lines
5.5 KiB
Python
205 lines
5.5 KiB
Python
import datetime
|
|
import re
|
|
import zoneinfo
|
|
from typing import TypeVar, Union
|
|
|
|
__all__ = [
|
|
"LOCAL_TIMEZONE",
|
|
"forward_to_next_weekday",
|
|
"int_to_weekday",
|
|
"localize",
|
|
"parse_dm_string",
|
|
"skip_weekends",
|
|
"str_to_date",
|
|
"str_to_month",
|
|
"str_to_weekday",
|
|
"time_string",
|
|
"tz_aware_now",
|
|
"tz_aware_today",
|
|
]
|
|
|
|
DateType = TypeVar("DateType", datetime.date, datetime.datetime)
|
|
|
|
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 and day_dt.weekday() == target_weekday:
|
|
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
|
|
"""Get the Dutch name of a weekday from the number"""
|
|
return ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"][number]
|
|
|
|
|
|
def localize(dt_instance: datetime.datetime, *, default_timezone="UTC") -> datetime.datetime:
|
|
"""Localize a datetime instance to my local timezone"""
|
|
if dt_instance.tzinfo is None:
|
|
dt_instance = dt_instance.replace(tzinfo=zoneinfo.ZoneInfo(default_timezone))
|
|
|
|
return dt_instance.astimezone(LOCAL_TIMEZONE)
|
|
|
|
|
|
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 = tz_aware_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])
|
|
if match is not None:
|
|
day = int(match.group())
|
|
month = str_to_month(spl[1])
|
|
return datetime.date(day=day, month=month, year=today.year)
|
|
|
|
# Month Day
|
|
match = re.search(r"\d+", spl[1])
|
|
if match is not None:
|
|
day = int(match.group())
|
|
month = str_to_month(spl[0])
|
|
return datetime.date(day=day, month=month, year=today.year)
|
|
|
|
# Unparseable
|
|
raise ValueError
|
|
|
|
|
|
def skip_weekends(dt_instance: datetime.date) -> datetime.date:
|
|
"""Fast-forward a date instance until its weekday is no longer a weekend"""
|
|
to_skip = (7 - dt_instance.weekday()) if dt_instance.weekday() > 4 else 0
|
|
return dt_instance + datetime.timedelta(days=to_skip)
|
|
|
|
|
|
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"""
|
|
# Allow passing multiple formats in a list
|
|
if isinstance(formats, str):
|
|
formats = [formats]
|
|
|
|
for format_str in formats:
|
|
try:
|
|
return datetime.datetime.strptime(date_str, format_str).date()
|
|
except ValueError:
|
|
continue
|
|
|
|
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 time_string(dt_instance: datetime.datetime) -> str:
|
|
"""Get an HH:MM representation of a datetime instance"""
|
|
return dt_instance.strftime("%H:%M")
|
|
|
|
|
|
def tz_aware_now() -> datetime.datetime:
|
|
"""Get the current date & time, but timezone-aware"""
|
|
return datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).astimezone(LOCAL_TIMEZONE)
|
|
|
|
|
|
def tz_aware_today() -> datetime.date:
|
|
"""Get the current day, but timezone-aware"""
|
|
return tz_aware_now().date()
|