from __future__ import annotations
import re
from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING, Literal
from .errors import ConvertTimeError, DurationError
from .internal import tp
from .internal.dc import discord
if TYPE_CHECKING:
from .i18n import LOCALE
__all__ = (
"convert_dt",
"convert_time",
"convert_to_seconds",
"dc_timestamp",
"set_utc",
)
[docs]
def set_utc(dt: datetime) -> datetime:
"""Set the timezone of a datetime object to UTC.
Parameters
----------
dt:
The datetime object to set the timezone of.
Returns
-------
:class:`datetime.datetime`
"""
return dt.replace(tzinfo=timezone.utc)
[docs]
def convert_time(
seconds: int | float,
relative: bool = True,
*,
locale: LOCALE | None = None,
) -> str:
"""Convert seconds to a human-readable time.
Parameters
----------
seconds:
The amount of seconds to convert.
relative:
Whether to use relative time. Defaults to ``True``.
.. hint::
This is only needed for German translation and will
not have any effect if the language is set to English.
>>> convert_time(450000, relative=True) # Relative: Seit 5 Tagen
'5 Tagen'
>>> convert_time(450000, relative=False) # Not relative: 5 Tage
'5 Tage'
locale:
The object to get the language from. Defaults to ``None``.
If not provided, the language will be set to the default language.
Returns
-------
:class:`str`
A human-readable time.
"""
if seconds < 60:
return f"{round(seconds)} {tp('sec', round(seconds), locale=locale)}"
minutes = seconds / 60
if minutes < 60:
return f"{round(minutes)} {tp('min', round(minutes), locale=locale)}"
hours = minutes / 60
if hours < 24:
return f"{round(hours)} {tp('hour', round(hours), locale=locale)}"
days = hours / 24
return f"{round(days)} {tp('day', round(days), relative=relative, locale=locale)}"
[docs]
def convert_dt(
dt: datetime | timedelta,
relative: bool = True,
*,
locale: LOCALE | None = None,
) -> str:
"""Convert :class:`datetime` or :class:`timedelta` to a human-readable time.
This function calls :func:`convert_time`.
Parameters
----------
dt:
The datetime or timedelta object to convert.
relative:
Whether to use relative time. Defaults to ``True``.
locale:
The interaction to get the language from. Defaults to ``None``.
If not provided, the language will be set to the default language.
Returns
-------
:class:`str`
A human-readable time.
"""
if isinstance(dt, timedelta):
return convert_time(abs(dt.total_seconds()), relative, locale=locale)
if isinstance(dt, datetime):
if dt.tzinfo is None:
dt = dt.astimezone()
return convert_time(
abs((dt - datetime.now(timezone.utc)).total_seconds()), relative, locale=locale
)
raise ValueError("dt must be datetime or timedelta")
[docs]
def dc_timestamp(
seconds: int | float, style: Literal["t", "T", "d", "D", "f", "F", "R"] = "R"
) -> str:
"""Convert seconds to a Discord timestamp.
Parameters
----------
seconds:
The amount of seconds to convert.
style: :class:`str`
The style of the timestamp. Defaults to ``R``.
For more information, see :func:`discord.utils.format_dt`.
Returns
-------
:class:`str`
A Discord timestamp.
"""
dt = datetime.now(tz=timezone.utc) + timedelta(seconds=seconds)
return discord.utils.format_dt(dt, style)
[docs]
def convert_to_seconds(
string: str, error: bool = False, default_unit: Literal["s", "m", "h", "d"] | None = "m"
) -> int:
"""Convert a string to seconds. Supports multiple units and decimal separators.
Parameters
----------
string:
The string to convert.
error:
Whether to raise an error if the string could not be converted. If set to ``False``,
the function will return ``0`` instead. Defaults to ``False``.
default_unit:
The default unit to use if no valid unit is specified. Defaults to ``m``.
If at least one valid unit is found, all numbers without a valid unit are ignored.
Returns
-------
:class:`int`
The amount of seconds.
Raises
------
:exc:`ConvertTimeError`
No valid number was found, or ``default_unit`` is ``None`` while no valid unit was found.
:exc:`DurationError`
The duration is too long.
Example
-------
>>> convert_to_seconds("1m 9s")
69
>>> convert_to_seconds("1.5m")
90
>>> convert_to_seconds("1,5 min")
90
>>> convert_to_seconds("1h 5m 10s")
3910
"""
units = {
"s": "seconds",
"m": "minutes",
"h": "hours",
"d": "days",
"t": "days",
"w": "weeks",
"mo": "months",
}
pattern = re.compile(r"(?P<value>\d+([.,]\d+)?) *(?P<unit>mo|[smhdtw]?)", flags=re.IGNORECASE)
matches = pattern.finditer(string)
no_unit = "0"
found_units = {}
for match in matches:
unit_char = match.group("unit").lower()
value = float(match.group("value").replace(",", "."))
unit = units.get(unit_char, no_unit)
found_units[unit] = value
if no_unit in found_units: # Number without valid unit found
if len(found_units) <= 1:
# No valid unit found -> Default unit is associated with the number
if default_unit is not None:
found_units[units[default_unit]] = found_units[no_unit]
del found_units[no_unit]
if error and not found_units:
raise ConvertTimeError(f"Could not convert '{string}' to seconds.")
if "months" in found_units:
found_units.setdefault("days", 0)
found_units["days"] = found_units["months"] * 30
del found_units["months"]
try:
return int(timedelta(**found_units).total_seconds())
except OverflowError:
if error:
raise DurationError(f"Duration '{string}' is too long.")
return 0