diff --git a/lib/arrow/__init__.py b/lib/arrow/__init__.py index 63dd6be9..bc597097 100644 --- a/lib/arrow/__init__.py +++ b/lib/arrow/__init__.py @@ -1,8 +1,39 @@ -# -*- coding: utf-8 -*- - +from ._version import __version__ +from .api import get, now, utcnow from .arrow import Arrow from .factory import ArrowFactory -from .api import get, now, utcnow +from .formatter import ( + FORMAT_ATOM, + FORMAT_COOKIE, + FORMAT_RFC822, + FORMAT_RFC850, + FORMAT_RFC1036, + FORMAT_RFC1123, + FORMAT_RFC2822, + FORMAT_RFC3339, + FORMAT_RSS, + FORMAT_W3C, +) +from .parser import ParserError -__version__ = '0.10.0' -VERSION = __version__ +# https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-no-implicit-reexport +# Mypy with --strict or --no-implicit-reexport requires an explicit reexport. +__all__ = [ + "__version__", + "get", + "now", + "utcnow", + "Arrow", + "ArrowFactory", + "FORMAT_ATOM", + "FORMAT_COOKIE", + "FORMAT_RFC822", + "FORMAT_RFC850", + "FORMAT_RFC1036", + "FORMAT_RFC1123", + "FORMAT_RFC2822", + "FORMAT_RFC3339", + "FORMAT_RSS", + "FORMAT_W3C", + "ParserError", +] diff --git a/lib/arrow/_version.py b/lib/arrow/_version.py new file mode 100644 index 00000000..c68196d1 --- /dev/null +++ b/lib/arrow/_version.py @@ -0,0 +1 @@ +__version__ = "1.2.0" diff --git a/lib/arrow/api.py b/lib/arrow/api.py index 16de39fe..d8ed24b9 100644 --- a/lib/arrow/api.py +++ b/lib/arrow/api.py @@ -1,55 +1,126 @@ -# -*- coding: utf-8 -*- -''' +""" Provides the default implementation of :class:`ArrowFactory ` methods for use as a module API. -''' +""" -from __future__ import absolute_import +from datetime import date, datetime +from datetime import tzinfo as dt_tzinfo +from time import struct_time +from typing import Any, List, Optional, Tuple, Type, Union, overload +from arrow.arrow import TZ_EXPR, Arrow +from arrow.constants import DEFAULT_LOCALE from arrow.factory import ArrowFactory - # internal default factory. _factory = ArrowFactory() +# TODO: Use Positional Only Argument (https://www.python.org/dev/peps/pep-0570/) +# after Python 3.7 deprecation -def get(*args, **kwargs): - ''' Implements the default :class:`ArrowFactory ` - ``get`` method. - ''' +@overload +def get( + *, + locale: str = DEFAULT_LOCALE, + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, +) -> Arrow: + ... # pragma: no cover + + +@overload +def get( + *args: int, + locale: str = DEFAULT_LOCALE, + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, +) -> Arrow: + ... # pragma: no cover + + +@overload +def get( + __obj: Union[ + Arrow, + datetime, + date, + struct_time, + dt_tzinfo, + int, + float, + str, + Tuple[int, int, int], + ], + *, + locale: str = DEFAULT_LOCALE, + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, +) -> Arrow: + ... # pragma: no cover + + +@overload +def get( + __arg1: Union[datetime, date], + __arg2: TZ_EXPR, + *, + locale: str = DEFAULT_LOCALE, + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, +) -> Arrow: + ... # pragma: no cover + + +@overload +def get( + __arg1: str, + __arg2: Union[str, List[str]], + *, + locale: str = DEFAULT_LOCALE, + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, +) -> Arrow: + ... # pragma: no cover + + +def get(*args: Any, **kwargs: Any) -> Arrow: + """Calls the default :class:`ArrowFactory ` ``get`` method.""" return _factory.get(*args, **kwargs) -def utcnow(): - ''' Implements the default :class:`ArrowFactory ` - ``utcnow`` method. - ''' +get.__doc__ = _factory.get.__doc__ + + +def utcnow() -> Arrow: + """Calls the default :class:`ArrowFactory ` ``utcnow`` method.""" return _factory.utcnow() -def now(tz=None): - ''' Implements the default :class:`ArrowFactory ` - ``now`` method. +utcnow.__doc__ = _factory.utcnow.__doc__ - ''' + +def now(tz: Optional[TZ_EXPR] = None) -> Arrow: + """Calls the default :class:`ArrowFactory ` ``now`` method.""" return _factory.now(tz) -def factory(type): - ''' Returns an :class:`.ArrowFactory` for the specified :class:`Arrow ` +now.__doc__ = _factory.now.__doc__ + + +def factory(type: Type[Arrow]) -> ArrowFactory: + """Returns an :class:`.ArrowFactory` for the specified :class:`Arrow ` or derived type. :param type: the type, :class:`Arrow ` or derived. - ''' + """ return ArrowFactory(type) -__all__ = ['get', 'utcnow', 'now', 'factory'] - +__all__ = ["get", "utcnow", "now", "factory"] diff --git a/lib/arrow/arrow.py b/lib/arrow/arrow.py index 131eec07..ad95cacd 100644 --- a/lib/arrow/arrow.py +++ b/lib/arrow/arrow.py @@ -1,25 +1,86 @@ -# -*- coding: utf-8 -*- -''' +""" Provides the :class:`Arrow ` class, an enhanced ``datetime`` replacement. -''' +""" -from __future__ import absolute_import -from datetime import datetime, timedelta, tzinfo +import calendar +import re +import sys +from datetime import date +from datetime import datetime as dt_datetime +from datetime import time as dt_time +from datetime import timedelta +from datetime import tzinfo as dt_tzinfo +from math import trunc +from time import struct_time +from typing import ( + Any, + ClassVar, + Generator, + Iterable, + List, + Mapping, + Optional, + Tuple, + Union, + cast, + overload, +) + from dateutil import tz as dateutil_tz from dateutil.relativedelta import relativedelta -import calendar -import sys -import warnings + +from arrow import formatter, locales, parser, util +from arrow.constants import DEFAULT_LOCALE, DEHUMANIZE_LOCALES +from arrow.locales import TimeFrameLiteral + +if sys.version_info < (3, 8): # pragma: no cover + from typing_extensions import Final, Literal +else: + from typing import Final, Literal # pragma: no cover -from arrow import util, locales, parser, formatter +TZ_EXPR = Union[dt_tzinfo, str] + +_T_FRAMES = Literal[ + "year", + "years", + "month", + "months", + "day", + "days", + "hour", + "hours", + "minute", + "minutes", + "second", + "seconds", + "microsecond", + "microseconds", + "week", + "weeks", + "quarter", + "quarters", +] + +_BOUNDS = Literal["[)", "()", "(]", "[]"] + +_GRANULARITY = Literal[ + "auto", + "second", + "minute", + "hour", + "day", + "week", + "month", + "year", +] -class Arrow(object): - '''An :class:`Arrow ` object. +class Arrow: + """An :class:`Arrow ` object. Implements the ``datetime`` interface, behaving as an aware ``datetime`` while implementing additional functionality. @@ -30,10 +91,18 @@ class Arrow(object): :param hour: (optional) the hour. Defaults to 0. :param minute: (optional) the minute, Defaults to 0. :param second: (optional) the second, Defaults to 0. - :param microsecond: (optional) the microsecond. Defaults 0. - :param tzinfo: (optional) the ``tzinfo`` object. Defaults to ``None``. + :param microsecond: (optional) the microsecond. Defaults to 0. + :param tzinfo: (optional) A timezone expression. Defaults to UTC. + :param fold: (optional) 0 or 1, used to disambiguate repeated wall times. Defaults to 0. - If tzinfo is None, it is assumed to be UTC on creation. + .. _tz-expr: + + Recognized timezone expressions: + + - A ``tzinfo`` object. + - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. + - A ``str`` in ISO 8601 style, as in '+07:00'. + - A ``str``, one of the following: 'local', 'utc', 'UTC'. Usage:: @@ -41,165 +110,351 @@ class Arrow(object): >>> arrow.Arrow(2013, 5, 5, 12, 30, 45) - ''' + """ - resolution = datetime.resolution + resolution: ClassVar[timedelta] = dt_datetime.resolution + min: ClassVar["Arrow"] + max: ClassVar["Arrow"] - _ATTRS = ['year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond'] - _ATTRS_PLURAL = ['{0}s'.format(a) for a in _ATTRS] - _MONTHS_PER_QUARTER = 3 + _ATTRS: Final[List[str]] = [ + "year", + "month", + "day", + "hour", + "minute", + "second", + "microsecond", + ] + _ATTRS_PLURAL: Final[List[str]] = [f"{a}s" for a in _ATTRS] + _MONTHS_PER_QUARTER: Final[int] = 3 + _SECS_PER_MINUTE: Final[int] = 60 + _SECS_PER_HOUR: Final[int] = 60 * 60 + _SECS_PER_DAY: Final[int] = 60 * 60 * 24 + _SECS_PER_WEEK: Final[int] = 60 * 60 * 24 * 7 + _SECS_PER_MONTH: Final[float] = 60 * 60 * 24 * 30.5 + _SECS_PER_YEAR: Final[int] = 60 * 60 * 24 * 365 - def __init__(self, year, month, day, hour=0, minute=0, second=0, microsecond=0, - tzinfo=None): + _SECS_MAP: Final[Mapping[TimeFrameLiteral, float]] = { + "second": 1.0, + "minute": _SECS_PER_MINUTE, + "hour": _SECS_PER_HOUR, + "day": _SECS_PER_DAY, + "week": _SECS_PER_WEEK, + "month": _SECS_PER_MONTH, + "year": _SECS_PER_YEAR, + } - if util.isstr(tzinfo): + _datetime: dt_datetime + + def __init__( + self, + year: int, + month: int, + day: int, + hour: int = 0, + minute: int = 0, + second: int = 0, + microsecond: int = 0, + tzinfo: Optional[TZ_EXPR] = None, + **kwargs: Any, + ) -> None: + if tzinfo is None: + tzinfo = dateutil_tz.tzutc() + # detect that tzinfo is a pytz object (issue #626) + elif ( + isinstance(tzinfo, dt_tzinfo) + and hasattr(tzinfo, "localize") + and hasattr(tzinfo, "zone") + and tzinfo.zone # type: ignore[attr-defined] + ): + tzinfo = parser.TzinfoParser.parse(tzinfo.zone) # type: ignore[attr-defined] + elif isinstance(tzinfo, str): tzinfo = parser.TzinfoParser.parse(tzinfo) - tzinfo = tzinfo or dateutil_tz.tzutc() - self._datetime = datetime(year, month, day, hour, minute, second, - microsecond, tzinfo) + fold = kwargs.get("fold", 0) + self._datetime = dt_datetime( + year, month, day, hour, minute, second, microsecond, tzinfo, fold=fold + ) # factories: single object, both original and from datetime. @classmethod - def now(cls, tzinfo=None): - '''Constructs an :class:`Arrow ` object, representing "now". + def now(cls, tzinfo: Optional[dt_tzinfo] = None) -> "Arrow": + """Constructs an :class:`Arrow ` object, representing "now" in the given + timezone. :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. - ''' + Usage:: - utc = datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc()) - dt = utc.astimezone(dateutil_tz.tzlocal() if tzinfo is None else tzinfo) + >>> arrow.now('Asia/Baku') + - return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, dt.tzinfo) + """ + + if tzinfo is None: + tzinfo = dateutil_tz.tzlocal() + + dt = dt_datetime.now(tzinfo) + + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + dt.tzinfo, + fold=getattr(dt, "fold", 0), + ) @classmethod - def utcnow(cls): - ''' Constructs an :class:`Arrow ` object, representing "now" in UTC + def utcnow(cls) -> "Arrow": + """Constructs an :class:`Arrow ` object, representing "now" in UTC time. - ''' + Usage:: - dt = datetime.utcnow() + >>> arrow.utcnow() + - return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, dateutil_tz.tzutc()) + """ + + dt = dt_datetime.now(dateutil_tz.tzutc()) + + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + dt.tzinfo, + fold=getattr(dt, "fold", 0), + ) @classmethod - def fromtimestamp(cls, timestamp, tzinfo=None): - ''' Constructs an :class:`Arrow ` object from a timestamp. + def fromtimestamp( + cls, + timestamp: Union[int, float, str], + tzinfo: Optional[TZ_EXPR] = None, + ) -> "Arrow": + """Constructs an :class:`Arrow ` object from a timestamp, converted to + the given timezone. :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. - ''' + """ - tzinfo = tzinfo or dateutil_tz.tzlocal() - timestamp = cls._get_timestamp_from_input(timestamp) - dt = datetime.fromtimestamp(timestamp, tzinfo) + if tzinfo is None: + tzinfo = dateutil_tz.tzlocal() + elif isinstance(tzinfo, str): + tzinfo = parser.TzinfoParser.parse(tzinfo) - return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, tzinfo) + if not util.is_timestamp(timestamp): + raise ValueError(f"The provided timestamp {timestamp!r} is invalid.") + + timestamp = util.normalize_timestamp(float(timestamp)) + dt = dt_datetime.fromtimestamp(timestamp, tzinfo) + + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + dt.tzinfo, + fold=getattr(dt, "fold", 0), + ) @classmethod - def utcfromtimestamp(cls, timestamp): - '''Constructs an :class:`Arrow ` object from a timestamp, in UTC time. + def utcfromtimestamp(cls, timestamp: Union[int, float, str]) -> "Arrow": + """Constructs an :class:`Arrow ` object from a timestamp, in UTC time. :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. - ''' + """ - timestamp = cls._get_timestamp_from_input(timestamp) - dt = datetime.utcfromtimestamp(timestamp) + if not util.is_timestamp(timestamp): + raise ValueError(f"The provided timestamp {timestamp!r} is invalid.") - return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, dateutil_tz.tzutc()) + timestamp = util.normalize_timestamp(float(timestamp)) + dt = dt_datetime.utcfromtimestamp(timestamp) + + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + dateutil_tz.tzutc(), + fold=getattr(dt, "fold", 0), + ) @classmethod - def fromdatetime(cls, dt, tzinfo=None): - ''' Constructs an :class:`Arrow ` object from a ``datetime`` and optional - ``tzinfo`` object. + def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": + """Constructs an :class:`Arrow ` object from a ``datetime`` and + optional replacement timezone. :param dt: the ``datetime`` - :param tzinfo: (optional) a ``tzinfo`` object. Defaults to UTC. + :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to ``dt``'s + timezone, or UTC if naive. - ''' + Usage:: - tzinfo = tzinfo or dt.tzinfo or dateutil_tz.tzutc() + >>> dt + datetime.datetime(2021, 4, 7, 13, 48, tzinfo=tzfile('/usr/share/zoneinfo/US/Pacific')) + >>> arrow.Arrow.fromdatetime(dt) + - return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, tzinfo) + """ + + if tzinfo is None: + if dt.tzinfo is None: + tzinfo = dateutil_tz.tzutc() + else: + tzinfo = dt.tzinfo + + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo, + fold=getattr(dt, "fold", 0), + ) @classmethod - def fromdate(cls, date, tzinfo=None): - ''' Constructs an :class:`Arrow ` object from a ``date`` and optional - ``tzinfo`` object. Time values are set to 0. + def fromdate(cls, date: date, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": + """Constructs an :class:`Arrow ` object from a ``date`` and optional + replacement timezone. All time values are set to 0. :param date: the ``date`` - :param tzinfo: (optional) a ``tzinfo`` object. Defaults to UTC. - ''' + :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to UTC. - tzinfo = tzinfo or dateutil_tz.tzutc() + """ + + if tzinfo is None: + tzinfo = dateutil_tz.tzutc() return cls(date.year, date.month, date.day, tzinfo=tzinfo) @classmethod - def strptime(cls, date_str, fmt, tzinfo=None): - ''' Constructs an :class:`Arrow ` object from a date string and format, - in the style of ``datetime.strptime``. + def strptime( + cls, date_str: str, fmt: str, tzinfo: Optional[TZ_EXPR] = None + ) -> "Arrow": + """Constructs an :class:`Arrow ` object from a date string and format, + in the style of ``datetime.strptime``. Optionally replaces the parsed timezone. :param date_str: the date string. - :param fmt: the format string. - :param tzinfo: (optional) an optional ``tzinfo`` - ''' + :param fmt: the format string using datetime format codes. + :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to the parsed + timezone if ``fmt`` contains a timezone directive, otherwise UTC. - dt = datetime.strptime(date_str, fmt) - tzinfo = tzinfo or dt.tzinfo + Usage:: - return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, tzinfo) + >>> arrow.Arrow.strptime('20-01-2019 15:49:10', '%d-%m-%Y %H:%M:%S') + + """ + + dt = dt_datetime.strptime(date_str, fmt) + if tzinfo is None: + tzinfo = dt.tzinfo + + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo, + fold=getattr(dt, "fold", 0), + ) + + @classmethod + def fromordinal(cls, ordinal: int) -> "Arrow": + """Constructs an :class:`Arrow ` object corresponding + to the Gregorian Ordinal. + + :param ordinal: an ``int`` corresponding to a Gregorian Ordinal. + + Usage:: + + >>> arrow.fromordinal(737741) + + + """ + + util.validate_ordinal(ordinal) + dt = dt_datetime.fromordinal(ordinal) + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + dt.tzinfo, + fold=getattr(dt, "fold", 0), + ) # factories: ranges and spans @classmethod - def range(cls, frame, start, end=None, tz=None, limit=None): - ''' Returns an array of :class:`Arrow ` objects, representing - an iteration of time between two inputs. + def range( + cls, + frame: _T_FRAMES, + start: Union["Arrow", dt_datetime], + end: Union["Arrow", dt_datetime, None] = None, + tz: Optional[TZ_EXPR] = None, + limit: Optional[int] = None, + ) -> Generator["Arrow", None, None]: + """Returns an iterator of :class:`Arrow ` objects, representing + points in time between two inputs. - :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). :param start: A datetime expression, the start of the range. :param end: (optional) A datetime expression, the end of the range. - :param tz: (optional) A timezone expression. Defaults to UTC. + :param tz: (optional) A :ref:`timezone expression `. Defaults to + ``start``'s timezone, or UTC if ``start`` is naive. :param limit: (optional) A maximum number of tuples to return. - **NOTE**: the **end** or **limit** must be provided. Call with **end** alone to - return the entire range, with **limit** alone to return a maximum # of results from the - start, and with both to cap a range at a maximum # of results. + **NOTE**: The ``end`` or ``limit`` must be provided. Call with ``end`` alone to + return the entire range. Call with ``limit`` alone to return a maximum # of results from + the start. Call with both to cap a range at a maximum # of results. - Supported frame values: year, quarter, month, week, day, hour, minute, second + **NOTE**: ``tz`` internally **replaces** the timezones of both ``start`` and ``end`` before + iterating. As such, either call with naive objects and ``tz``, or aware objects from the + same timezone and no ``tz``. + + Supported frame values: year, quarter, month, week, day, hour, minute, second, microsecond. Recognized datetime expressions: - An :class:`Arrow ` object. - A ``datetime`` object. - Recognized timezone expressions: - - - A ``tzinfo`` object. - - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. - - A ``str`` in ISO-8601 style, as in '+07:00'. - - A ``str``, one of the following: 'local', 'utc', 'UTC'. - - Usage: + Usage:: >>> start = datetime(2013, 5, 5, 12, 30) >>> end = datetime(2013, 5, 5, 17, 15) >>> for r in arrow.Arrow.range('hour', start, end): - ... print repr(r) + ... print(repr(r)) ... @@ -207,7 +462,17 @@ class Arrow(object): - ''' + **NOTE**: Unlike Python's ``range``, ``end`` *may* be included in the returned iterator:: + + >>> start = datetime(2013, 5, 5, 12, 30) + >>> end = datetime(2013, 5, 5, 13, 30) + >>> for r in arrow.Arrow.range('hour', start, end): + ... print(repr(r)) + ... + + + + """ _, frame_relative, relative_steps = cls._get_frames(frame) @@ -218,31 +483,259 @@ class Arrow(object): end = cls._get_datetime(end).replace(tzinfo=tzinfo) current = cls.fromdatetime(start) - results = [] + original_day = start.day + day_is_clipped = False + i = 0 - while current <= end and len(results) < limit: - results.append(current) + while current <= end and i < limit: + i += 1 + yield current values = [getattr(current, f) for f in cls._ATTRS] - current = cls(*values, tzinfo=tzinfo) + relativedelta(**{frame_relative: relative_steps}) + current = cls(*values, tzinfo=tzinfo).shift( # type: ignore + **{frame_relative: relative_steps} + ) - return results + if frame in ["month", "quarter", "year"] and current.day < original_day: + day_is_clipped = True + if day_is_clipped and not cls._is_last_day_of_month(current): + current = current.replace(day=original_day) - @classmethod - def span_range(cls, frame, start, end, tz=None, limit=None): - ''' Returns an array of tuples, each :class:`Arrow ` objects, - representing a series of timespans between two inputs. + def span( + self, + frame: _T_FRAMES, + count: int = 1, + bounds: _BOUNDS = "[)", + exact: bool = False, + week_start: int = 1, + ) -> Tuple["Arrow", "Arrow"]: + """Returns a tuple of two new :class:`Arrow ` objects, representing the timespan + of the :class:`Arrow ` object in a given timeframe. :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + :param count: (optional) the number of frames to span. + :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies + whether to include or exclude the start and end values in the span. '(' excludes + the start, '[' includes the start, ')' excludes the end, and ']' includes the end. + If the bounds are not specified, the default bound '[)' is used. + :param exact: (optional) whether to have the start of the timespan begin exactly + at the time specified by ``start`` and the end of the timespan truncated + so as not to extend beyond ``end``. + :param week_start: (optional) only used in combination with the week timeframe. Follows isoweekday() where + Monday is 1 and Sunday is 7. + + Supported frame values: year, quarter, month, week, day, hour, minute, second. + + Usage:: + + >>> arrow.utcnow() + + + >>> arrow.utcnow().span('hour') + (, ) + + >>> arrow.utcnow().span('day') + (, ) + + >>> arrow.utcnow().span('day', count=2) + (, ) + + >>> arrow.utcnow().span('day', bounds='[]') + (, ) + + >>> arrow.utcnow().span('week') + (, ) + + >>> arrow.utcnow().span('week', week_start=6) + (, ) + + """ + if not 1 <= week_start <= 7: + raise ValueError("week_start argument must be between 1 and 7.") + + util.validate_bounds(bounds) + + frame_absolute, frame_relative, relative_steps = self._get_frames(frame) + + if frame_absolute == "week": + attr = "day" + elif frame_absolute == "quarter": + attr = "month" + else: + attr = frame_absolute + + floor = self + if not exact: + index = self._ATTRS.index(attr) + frames = self._ATTRS[: index + 1] + + values = [getattr(self, f) for f in frames] + + for _ in range(3 - len(values)): + values.append(1) + + floor = self.__class__(*values, tzinfo=self.tzinfo) # type: ignore + + if frame_absolute == "week": + # if week_start is greater than self.isoweekday() go back one week by setting delta = 7 + delta = 7 if week_start > self.isoweekday() else 0 + floor = floor.shift(days=-(self.isoweekday() - week_start) - delta) + elif frame_absolute == "quarter": + floor = floor.shift(months=-((self.month - 1) % 3)) + + ceil = floor.shift(**{frame_relative: count * relative_steps}) + + if bounds[0] == "(": + floor = floor.shift(microseconds=+1) + + if bounds[1] == ")": + ceil = ceil.shift(microseconds=-1) + + return floor, ceil + + def floor(self, frame: _T_FRAMES) -> "Arrow": + """Returns a new :class:`Arrow ` object, representing the "floor" + of the timespan of the :class:`Arrow ` object in a given timeframe. + Equivalent to the first element in the 2-tuple returned by + :func:`span `. + + :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + + Usage:: + + >>> arrow.utcnow().floor('hour') + + + """ + + return self.span(frame)[0] + + def ceil(self, frame: _T_FRAMES) -> "Arrow": + """Returns a new :class:`Arrow ` object, representing the "ceiling" + of the timespan of the :class:`Arrow ` object in a given timeframe. + Equivalent to the second element in the 2-tuple returned by + :func:`span `. + + :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + + Usage:: + + >>> arrow.utcnow().ceil('hour') + + + """ + + return self.span(frame)[1] + + @classmethod + def span_range( + cls, + frame: _T_FRAMES, + start: dt_datetime, + end: dt_datetime, + tz: Optional[TZ_EXPR] = None, + limit: Optional[int] = None, + bounds: _BOUNDS = "[)", + exact: bool = False, + ) -> Iterable[Tuple["Arrow", "Arrow"]]: + """Returns an iterator of tuples, each :class:`Arrow ` objects, + representing a series of timespans between two inputs. + + :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). :param start: A datetime expression, the start of the range. :param end: (optional) A datetime expression, the end of the range. - :param tz: (optional) A timezone expression. Defaults to UTC. + :param tz: (optional) A :ref:`timezone expression `. Defaults to + ``start``'s timezone, or UTC if ``start`` is naive. :param limit: (optional) A maximum number of tuples to return. + :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies + whether to include or exclude the start and end values in each span in the range. '(' excludes + the start, '[' includes the start, ')' excludes the end, and ']' includes the end. + If the bounds are not specified, the default bound '[)' is used. + :param exact: (optional) whether to have the first timespan start exactly + at the time specified by ``start`` and the final span truncated + so as not to extend beyond ``end``. - **NOTE**: the **end** or **limit** must be provided. Call with **end** alone to - return the entire range, with **limit** alone to return a maximum # of results from the - start, and with both to cap a range at a maximum # of results. + **NOTE**: The ``end`` or ``limit`` must be provided. Call with ``end`` alone to + return the entire range. Call with ``limit`` alone to return a maximum # of results from + the start. Call with both to cap a range at a maximum # of results. + + **NOTE**: ``tz`` internally **replaces** the timezones of both ``start`` and ``end`` before + iterating. As such, either call with naive objects and ``tz``, or aware objects from the + same timezone and no ``tz``. + + Supported frame values: year, quarter, month, week, day, hour, minute, second, microsecond. + + Recognized datetime expressions: + + - An :class:`Arrow ` object. + - A ``datetime`` object. + + **NOTE**: Unlike Python's ``range``, ``end`` will *always* be included in the returned + iterator of timespans. + + Usage: + + >>> start = datetime(2013, 5, 5, 12, 30) + >>> end = datetime(2013, 5, 5, 17, 15) + >>> for r in arrow.Arrow.span_range('hour', start, end): + ... print(r) + ... + (, ) + (, ) + (, ) + (, ) + (, ) + (, ) + + """ + + tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz) + start = cls.fromdatetime(start, tzinfo).span(frame, exact=exact)[0] + end = cls.fromdatetime(end, tzinfo) + _range = cls.range(frame, start, end, tz, limit) + if not exact: + for r in _range: + yield r.span(frame, bounds=bounds, exact=exact) + + for r in _range: + floor, ceil = r.span(frame, bounds=bounds, exact=exact) + if ceil > end: + ceil = end + if bounds[1] == ")": + ceil += relativedelta(microseconds=-1) + if floor == end: + break + elif floor + relativedelta(microseconds=-1) == end: + break + yield floor, ceil + + @classmethod + def interval( + cls, + frame: _T_FRAMES, + start: dt_datetime, + end: dt_datetime, + interval: int = 1, + tz: Optional[TZ_EXPR] = None, + bounds: _BOUNDS = "[)", + exact: bool = False, + ) -> Iterable[Tuple["Arrow", "Arrow"]]: + """Returns an iterator of tuples, each :class:`Arrow ` objects, + representing a series of intervals between two inputs. + + :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). + :param start: A datetime expression, the start of the range. + :param end: (optional) A datetime expression, the end of the range. + :param interval: (optional) Time interval for the given time frame. + :param tz: (optional) A timezone expression. Defaults to UTC. + :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies + whether to include or exclude the start and end values in the intervals. '(' excludes + the start, '[' includes the start, ')' excludes the end, and ']' includes the end. + If the bounds are not specified, the default bound '[)' is used. + :param exact: (optional) whether to have the first timespan start exactly + at the time specified by ``start`` and the final interval truncated + so as not to extend beyond ``end``. Supported frame values: year, quarter, month, week, day, hour, minute, second @@ -255,192 +748,248 @@ class Arrow(object): - A ``tzinfo`` object. - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. - - A ``str`` in ISO-8601 style, as in '+07:00'. + - A ``str`` in ISO 8601 style, as in '+07:00'. - A ``str``, one of the following: 'local', 'utc', 'UTC'. Usage: >>> start = datetime(2013, 5, 5, 12, 30) >>> end = datetime(2013, 5, 5, 17, 15) - >>> for r in arrow.Arrow.span_range('hour', start, end): + >>> for r in arrow.Arrow.interval('hour', start, end, 2): ... print r ... - (, ) - (, ) - (, ) - (, ) - (, ) - - ''' - tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz) - start = cls.fromdatetime(start, tzinfo).span(frame)[0] - _range = cls.range(frame, start, end, tz, limit) - return [r.span(frame) for r in _range] + (, ) + (, ) + (, ) + """ + if interval < 1: + raise ValueError("interval has to be a positive integer") + spanRange = iter( + cls.span_range(frame, start, end, tz, bounds=bounds, exact=exact) + ) + while True: + try: + intvlStart, intvlEnd = next(spanRange) + for _ in range(interval - 1): + try: + _, intvlEnd = next(spanRange) + except StopIteration: + continue + yield intvlStart, intvlEnd + except StopIteration: + return # representations - def __repr__(self): + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.__str__()}]>" - dt = self._datetime - attrs = ', '.join([str(i) for i in [dt.year, dt.month, dt.day, dt.hour, dt.minute, - dt.second, dt.microsecond]]) - - return '<{0} [{1}]>'.format(self.__class__.__name__, self.__str__()) - - def __str__(self): + def __str__(self) -> str: return self._datetime.isoformat() - def __format__(self, formatstr): + def __format__(self, formatstr: str) -> str: if len(formatstr) > 0: return self.format(formatstr) return str(self) - def __hash__(self): + def __hash__(self) -> int: return self._datetime.__hash__() + # attributes and properties - # attributes & properties + def __getattr__(self, name: str) -> int: - def __getattr__(self, name): - - if name == 'week': + if name == "week": return self.isocalendar()[1] - if name == 'quarter': - return int((self.month-1)/self._MONTHS_PER_QUARTER) + 1 + if name == "quarter": + return int((self.month - 1) / self._MONTHS_PER_QUARTER) + 1 - if not name.startswith('_'): - value = getattr(self._datetime, name, None) + if not name.startswith("_"): + value: Optional[int] = getattr(self._datetime, name, None) if value is not None: return value - return object.__getattribute__(self, name) + return cast(int, object.__getattribute__(self, name)) @property - def tzinfo(self): - ''' Gets the ``tzinfo`` of the :class:`Arrow ` object. ''' + def tzinfo(self) -> dt_tzinfo: + """Gets the ``tzinfo`` of the :class:`Arrow ` object. - return self._datetime.tzinfo + Usage:: - @tzinfo.setter - def tzinfo(self, tzinfo): - ''' Sets the ``tzinfo`` of the :class:`Arrow ` object. ''' + >>> arw=arrow.utcnow() + >>> arw.tzinfo + tzutc() - self._datetime = self._datetime.replace(tzinfo=tzinfo) + """ + + # In Arrow, `_datetime` cannot be naive. + return cast(dt_tzinfo, self._datetime.tzinfo) @property - def datetime(self): - ''' Returns a datetime representation of the :class:`Arrow ` object. ''' + def datetime(self) -> dt_datetime: + """Returns a datetime representation of the :class:`Arrow ` object. + + Usage:: + + >>> arw=arrow.utcnow() + >>> arw.datetime + datetime.datetime(2019, 1, 24, 16, 35, 27, 276649, tzinfo=tzutc()) + + """ return self._datetime @property - def naive(self): - ''' Returns a naive datetime representation of the :class:`Arrow ` object. ''' + def naive(self) -> dt_datetime: + """Returns a naive datetime representation of the :class:`Arrow ` + object. + + Usage:: + + >>> nairobi = arrow.now('Africa/Nairobi') + >>> nairobi + + >>> nairobi.naive + datetime.datetime(2019, 1, 23, 19, 27, 12, 297999) + + """ return self._datetime.replace(tzinfo=None) - @property - def timestamp(self): - ''' Returns a timestamp representation of the :class:`Arrow ` object. ''' + def timestamp(self) -> float: + """Returns a timestamp representation of the :class:`Arrow ` object, in + UTC time. - return calendar.timegm(self._datetime.utctimetuple()) + Usage:: + + >>> arrow.utcnow().timestamp() + 1616882340.256501 + + """ + + return self._datetime.timestamp() @property - def float_timestamp(self): - ''' Returns a floating-point representation of the :class:`Arrow ` object. ''' + def int_timestamp(self) -> int: + """Returns an integer timestamp representation of the :class:`Arrow ` object, in + UTC time. - return self.timestamp + float(self.microsecond) / 1000000 + Usage:: + >>> arrow.utcnow().int_timestamp + 1548260567 + + """ + + return int(self.timestamp()) + + @property + def float_timestamp(self) -> float: + """Returns a floating-point timestamp representation of the :class:`Arrow ` + object, in UTC time. + + Usage:: + + >>> arrow.utcnow().float_timestamp + 1548260516.830896 + + """ + + return self.timestamp() + + @property + def fold(self) -> int: + """Returns the ``fold`` value of the :class:`Arrow ` object.""" + + return self._datetime.fold + + @property + def ambiguous(self) -> bool: + """Indicates whether the :class:`Arrow ` object is a repeated wall time in the current + timezone. + + """ + + return dateutil_tz.datetime_ambiguous(self._datetime) + + @property + def imaginary(self) -> bool: + """Indicates whether the :class: `Arrow ` object exists in the current timezone.""" + + return not dateutil_tz.datetime_exists(self._datetime) # mutation and duplication. - def clone(self): - ''' Returns a new :class:`Arrow ` object, cloned from the current one. + def clone(self) -> "Arrow": + """Returns a new :class:`Arrow ` object, cloned from the current one. Usage: >>> arw = arrow.utcnow() >>> cloned = arw.clone() - ''' + """ return self.fromdatetime(self._datetime) - def replace(self, **kwargs): - ''' Returns a new :class:`Arrow ` object with attributes updated + def replace(self, **kwargs: Any) -> "Arrow": + """Returns a new :class:`Arrow ` object with attributes updated according to inputs. - Use single property names to set their value absolutely: + Use property names to set their value absolutely:: - >>> import arrow - >>> arw = arrow.utcnow() - >>> arw - - >>> arw.replace(year=2014, month=6) - + >>> import arrow + >>> arw = arrow.utcnow() + >>> arw + + >>> arw.replace(year=2014, month=6) + - You can also provide a timezone expression can also be replaced: + You can also replace the timezone without conversion, using a + :ref:`timezone expression `:: - >>> arw.replace(tzinfo=tz.tzlocal()) - + >>> arw.replace(tzinfo=tz.tzlocal()) + - Use plural property names to shift their current value relatively (**deprecated**): - - >>> arw.replace(years=1, months=-1) - - - Recognized timezone expressions: - - - A ``tzinfo`` object. - - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. - - A ``str`` in ISO-8601 style, as in '+07:00'. - - A ``str``, one of the following: 'local', 'utc', 'UTC'. - - ''' + """ absolute_kwargs = {} - relative_kwargs = {} # TODO: DEPRECATED; remove in next release for key, value in kwargs.items(): if key in self._ATTRS: absolute_kwargs[key] = value - elif key in self._ATTRS_PLURAL or key in ['weeks', 'quarters']: - # TODO: DEPRECATED - warnings.warn("replace() with plural property to shift value" - "is deprecated, use shift() instead", - DeprecationWarning) - relative_kwargs[key] = value - elif key in ['week', 'quarter']: - raise AttributeError('setting absolute {0} is not supported'.format(key)) - elif key !='tzinfo': - raise AttributeError('unknown attribute: "{0}"'.format(key)) - - # core datetime does not support quarters, translate to months. - relative_kwargs.setdefault('months', 0) - relative_kwargs['months'] += relative_kwargs.pop('quarters', 0) * self._MONTHS_PER_QUARTER + elif key in ["week", "quarter"]: + raise ValueError(f"Setting absolute {key} is not supported.") + elif key not in ["tzinfo", "fold"]: + raise ValueError(f"Unknown attribute: {key!r}.") current = self._datetime.replace(**absolute_kwargs) - current += relativedelta(**relative_kwargs) # TODO: DEPRECATED - tzinfo = kwargs.get('tzinfo') + tzinfo = kwargs.get("tzinfo") if tzinfo is not None: tzinfo = self._get_tzinfo(tzinfo) current = current.replace(tzinfo=tzinfo) + fold = kwargs.get("fold") + + if fold is not None: + current = current.replace(fold=fold) + return self.fromdatetime(current) - def shift(self, **kwargs): - ''' Returns a new :class:`Arrow ` object with attributes updated + def shift(self, **kwargs: Any) -> "Arrow": + """Returns a new :class:`Arrow ` object with attributes updated according to inputs. - Use plural property names to shift their current value relatively: + Use pluralized property names to relatively shift their current value: >>> import arrow >>> arw = arrow.utcnow() @@ -449,37 +998,54 @@ class Arrow(object): >>> arw.shift(years=1, months=-1) - ''' + Day-of-the-week relative shifting can use either Python's weekday numbers + (Monday = 0, Tuesday = 1 .. Sunday = 6) or using dateutil.relativedelta's + day instances (MO, TU .. SU). When using weekday numbers, the returned + date will always be greater than or equal to the starting date. + + Using the above code (which is a Saturday) and asking it to shift to Saturday: + + >>> arw.shift(weekday=5) + + + While asking for a Monday: + + >>> arw.shift(weekday=0) + + + """ relative_kwargs = {} + additional_attrs = ["weeks", "quarters", "weekday"] for key, value in kwargs.items(): - if key in self._ATTRS_PLURAL or key in ['weeks', 'quarters']: + if key in self._ATTRS_PLURAL or key in additional_attrs: relative_kwargs[key] = value else: - raise AttributeError() + supported_attr = ", ".join(self._ATTRS_PLURAL + additional_attrs) + raise ValueError( + f"Invalid shift time frame. Please select one of the following: {supported_attr}." + ) # core datetime does not support quarters, translate to months. - relative_kwargs.setdefault('months', 0) - relative_kwargs['months'] += relative_kwargs.pop('quarters', 0) * self._MONTHS_PER_QUARTER + relative_kwargs.setdefault("months", 0) + relative_kwargs["months"] += ( + relative_kwargs.pop("quarters", 0) * self._MONTHS_PER_QUARTER + ) current = self._datetime + relativedelta(**relative_kwargs) + if not dateutil_tz.datetime_exists(current): + current = dateutil_tz.resolve_imaginary(current) + return self.fromdatetime(current) - def to(self, tz): - ''' Returns a new :class:`Arrow ` object, converted + def to(self, tz: TZ_EXPR) -> "Arrow": + """Returns a new :class:`Arrow ` object, converted to the target timezone. - :param tz: an expression representing a timezone. - - Recognized timezone expressions: - - - A ``tzinfo`` object. - - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. - - A ``str`` in ISO-8601 style, as in '+07:00'. - - A ``str``, one of the following: 'local', 'utc', 'UTC'. + :param tz: A :ref:`timezone expression `. Usage:: @@ -502,110 +1068,35 @@ class Arrow(object): >>> utc.to('local').to('utc') - ''' + """ - if not isinstance(tz, tzinfo): + if not isinstance(tz, dt_tzinfo): tz = parser.TzinfoParser.parse(tz) dt = self._datetime.astimezone(tz) - return self.__class__(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, dt.tzinfo) + return self.__class__( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + dt.tzinfo, + fold=getattr(dt, "fold", 0), + ) - def span(self, frame, count=1): - ''' Returns two new :class:`Arrow ` objects, representing the timespan - of the :class:`Arrow ` object in a given timeframe. + # string output and formatting - :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). - :param count: (optional) the number of frames to span. - - Supported frame values: year, quarter, month, week, day, hour, minute, second - - Usage:: - - >>> arrow.utcnow() - - - >>> arrow.utcnow().span('hour') - (, ) - - >>> arrow.utcnow().span('day') - (, ) - - >>> arrow.utcnow().span('day', count=2) - (, ) - - ''' - - frame_absolute, frame_relative, relative_steps = self._get_frames(frame) - - if frame_absolute == 'week': - attr = 'day' - elif frame_absolute == 'quarter': - attr = 'month' - else: - attr = frame_absolute - - index = self._ATTRS.index(attr) - frames = self._ATTRS[:index + 1] - - values = [getattr(self, f) for f in frames] - - for i in range(3 - len(values)): - values.append(1) - - floor = self.__class__(*values, tzinfo=self.tzinfo) - - if frame_absolute == 'week': - floor = floor + relativedelta(days=-(self.isoweekday() - 1)) - elif frame_absolute == 'quarter': - floor = floor + relativedelta(months=-((self.month - 1) % 3)) - - ceil = floor + relativedelta( - **{frame_relative: count * relative_steps}) + relativedelta(microseconds=-1) - - return floor, ceil - - def floor(self, frame): - ''' Returns a new :class:`Arrow ` object, representing the "floor" - of the timespan of the :class:`Arrow ` object in a given timeframe. - Equivalent to the first element in the 2-tuple returned by - :func:`span `. - - :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). - - Usage:: - - >>> arrow.utcnow().floor('hour') - - ''' - - return self.span(frame)[0] - - def ceil(self, frame): - ''' Returns a new :class:`Arrow ` object, representing the "ceiling" - of the timespan of the :class:`Arrow ` object in a given timeframe. - Equivalent to the second element in the 2-tuple returned by - :func:`span `. - - :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). - - Usage:: - - >>> arrow.utcnow().ceil('hour') - - ''' - - return self.span(frame)[1] - - - # string output and formatting. - - def format(self, fmt='YYYY-MM-DD HH:mm:ssZZ', locale='en_us'): - ''' Returns a string representation of the :class:`Arrow ` object, - formatted according to a format string. + def format( + self, fmt: str = "YYYY-MM-DD HH:mm:ssZZ", locale: str = DEFAULT_LOCALE + ) -> str: + """Returns a string representation of the :class:`Arrow ` object, + formatted according to the provided format string. :param fmt: the format string. + :param locale: the locale to format. Usage:: @@ -621,313 +1112,749 @@ class Arrow(object): >>> arrow.utcnow().format() '2013-05-09 03:56:47 -00:00' - ''' + """ return formatter.DateTimeFormatter(locale).format(self._datetime, fmt) - - def humanize(self, other=None, locale='en_us', only_distance=False): - ''' Returns a localized, humanized representation of a relative difference in time. + def humanize( + self, + other: Union["Arrow", dt_datetime, None] = None, + locale: str = DEFAULT_LOCALE, + only_distance: bool = False, + granularity: Union[_GRANULARITY, List[_GRANULARITY]] = "auto", + ) -> str: + """Returns a localized, humanized representation of a relative difference in time. :param other: (optional) an :class:`Arrow ` or ``datetime`` object. Defaults to now in the current :class:`Arrow ` object's timezone. - :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en_us'. + :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en-us'. :param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part. + :param granularity: (optional) defines the precision of the output. Set it to strings 'second', 'minute', + 'hour', 'day', 'week', 'month' or 'year' or a list of any combination of these strings Usage:: - >>> earlier = arrow.utcnow().replace(hours=-2) + >>> earlier = arrow.utcnow().shift(hours=-2) >>> earlier.humanize() '2 hours ago' - >>> later = later = earlier.replace(hours=4) + >>> later = earlier.shift(hours=4) >>> later.humanize(earlier) 'in 4 hours' - ''' + """ + locale_name = locale locale = locales.get_locale(locale) if other is None: - utc = datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc()) + utc = dt_datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc()) dt = utc.astimezone(self._datetime.tzinfo) elif isinstance(other, Arrow): dt = other._datetime - elif isinstance(other, datetime): + elif isinstance(other, dt_datetime): if other.tzinfo is None: dt = other.replace(tzinfo=self._datetime.tzinfo) else: dt = other.astimezone(self._datetime.tzinfo) else: - raise TypeError() + raise TypeError( + f"Invalid 'other' argument of type {type(other).__name__!r}. " + "Argument must be of type None, Arrow, or datetime." + ) - delta = int(util.total_seconds(self._datetime - dt)) - sign = -1 if delta < 0 else 1 - diff = abs(delta) - delta = diff + if isinstance(granularity, list) and len(granularity) == 1: + granularity = granularity[0] - if diff < 10: - return locale.describe('now', only_distance=only_distance) + _delta = int(round((self._datetime - dt).total_seconds())) + sign = -1 if _delta < 0 else 1 + delta_second = diff = abs(_delta) - if diff < 45: - return locale.describe('seconds', sign, only_distance=only_distance) + try: + if granularity == "auto": + if diff < 10: + return locale.describe("now", only_distance=only_distance) - elif diff < 90: - return locale.describe('minute', sign, only_distance=only_distance) - elif diff < 2700: - minutes = sign * int(max(delta / 60, 2)) - return locale.describe('minutes', minutes, only_distance=only_distance) + if diff < self._SECS_PER_MINUTE: + seconds = sign * delta_second + return locale.describe( + "seconds", seconds, only_distance=only_distance + ) - elif diff < 5400: - return locale.describe('hour', sign, only_distance=only_distance) - elif diff < 79200: - hours = sign * int(max(delta / 3600, 2)) - return locale.describe('hours', hours, only_distance=only_distance) + elif diff < self._SECS_PER_MINUTE * 2: + return locale.describe("minute", sign, only_distance=only_distance) + elif diff < self._SECS_PER_HOUR: + minutes = sign * max(delta_second // self._SECS_PER_MINUTE, 2) + return locale.describe( + "minutes", minutes, only_distance=only_distance + ) - elif diff < 129600: - return locale.describe('day', sign, only_distance=only_distance) - elif diff < 2160000: - days = sign * int(max(delta / 86400, 2)) - return locale.describe('days', days, only_distance=only_distance) + elif diff < self._SECS_PER_HOUR * 2: + return locale.describe("hour", sign, only_distance=only_distance) + elif diff < self._SECS_PER_DAY: + hours = sign * max(delta_second // self._SECS_PER_HOUR, 2) + return locale.describe("hours", hours, only_distance=only_distance) + elif diff < self._SECS_PER_DAY * 2: + return locale.describe("day", sign, only_distance=only_distance) + elif diff < self._SECS_PER_WEEK: + days = sign * max(delta_second // self._SECS_PER_DAY, 2) + return locale.describe("days", days, only_distance=only_distance) - elif diff < 3888000: - return locale.describe('month', sign, only_distance=only_distance) - elif diff < 29808000: - self_months = self._datetime.year * 12 + self._datetime.month - other_months = dt.year * 12 + dt.month + elif diff < self._SECS_PER_WEEK * 2: + return locale.describe("week", sign, only_distance=only_distance) + elif diff < self._SECS_PER_MONTH: + weeks = sign * max(delta_second // self._SECS_PER_WEEK, 2) + return locale.describe("weeks", weeks, only_distance=only_distance) - months = sign * int(max(abs(other_months - self_months), 2)) + elif diff < self._SECS_PER_MONTH * 2: + return locale.describe("month", sign, only_distance=only_distance) + elif diff < self._SECS_PER_YEAR: + # TODO revisit for humanization during leap years + self_months = self._datetime.year * 12 + self._datetime.month + other_months = dt.year * 12 + dt.month - return locale.describe('months', months, only_distance=only_distance) + months = sign * max(abs(other_months - self_months), 2) - elif diff < 47260800: - return locale.describe('year', sign, only_distance=only_distance) + return locale.describe( + "months", months, only_distance=only_distance + ) + + elif diff < self._SECS_PER_YEAR * 2: + return locale.describe("year", sign, only_distance=only_distance) + else: + years = sign * max(delta_second // self._SECS_PER_YEAR, 2) + return locale.describe("years", years, only_distance=only_distance) + + elif isinstance(granularity, str): + granularity = cast(TimeFrameLiteral, granularity) # type: ignore[assignment] + + if granularity == "second": + delta = sign * float(delta_second) + if abs(delta) < 2: + return locale.describe("now", only_distance=only_distance) + elif granularity == "minute": + delta = sign * delta_second / self._SECS_PER_MINUTE + elif granularity == "hour": + delta = sign * delta_second / self._SECS_PER_HOUR + elif granularity == "day": + delta = sign * delta_second / self._SECS_PER_DAY + elif granularity == "week": + delta = sign * delta_second / self._SECS_PER_WEEK + elif granularity == "month": + delta = sign * delta_second / self._SECS_PER_MONTH + elif granularity == "year": + delta = sign * delta_second / self._SECS_PER_YEAR + else: + raise ValueError( + "Invalid level of granularity. " + "Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'." + ) + + if trunc(abs(delta)) != 1: + granularity += "s" # type: ignore + return locale.describe(granularity, delta, only_distance=only_distance) + + else: + timeframes: List[Tuple[TimeFrameLiteral, float]] = [] + + def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float: + if _frame in granularity: + value = sign * _delta / self._SECS_MAP[_frame] + _delta %= self._SECS_MAP[_frame] + if trunc(abs(value)) != 1: + timeframes.append( + (cast(TimeFrameLiteral, _frame + "s"), value) + ) + else: + timeframes.append((_frame, value)) + return _delta + + delta = float(delta_second) + frames: Tuple[TimeFrameLiteral, ...] = ( + "year", + "month", + "week", + "day", + "hour", + "minute", + "second", + ) + for frame in frames: + delta = gather_timeframes(delta, frame) + + if len(timeframes) < len(granularity): + raise ValueError( + "Invalid level of granularity. " + "Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'." + ) + + return locale.describe_multi(timeframes, only_distance=only_distance) + + except KeyError as e: + raise ValueError( + f"Humanization of the {e} granularity is not currently translated in the {locale_name!r} locale. " + "Please consider making a contribution to this locale." + ) + + def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": + """Returns a new :class:`Arrow ` object, that represents + the time difference relative to the attrbiutes of the + :class:`Arrow ` object. + + :param timestring: a ``str`` representing a humanized relative time. + :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en-us'. + + Usage:: + + >>> arw = arrow.utcnow() + >>> arw + + >>> earlier = arw.dehumanize("2 days ago") + >>> earlier + + + >>> arw = arrow.utcnow() + >>> arw + + >>> later = arw.dehumanize("in a month") + >>> later + + + """ + + # Create a locale object based off given local + locale_obj = locales.get_locale(locale) + + # Check to see if locale is supported + normalized_locale_name = locale.lower().replace("_", "-") + + if normalized_locale_name not in DEHUMANIZE_LOCALES: + raise ValueError( + f"Dehumanize does not currently support the {locale} locale, please consider making a contribution to add support for this locale." + ) + + current_time = self.fromdatetime(self._datetime) + + # Create an object containing the relative time info + time_object_info = dict.fromkeys( + ["seconds", "minutes", "hours", "days", "weeks", "months", "years"], 0 + ) + + # Create an object representing if unit has been seen + unit_visited = dict.fromkeys( + ["now", "seconds", "minutes", "hours", "days", "weeks", "months", "years"], + False, + ) + + # Create a regex pattern object for numbers + num_pattern = re.compile(r"\d+") + + # Search input string for each time unit within locale + for unit, unit_object in locale_obj.timeframes.items(): + + # Need to check the type of unit_object to create the correct dictionary + if isinstance(unit_object, Mapping): + strings_to_search = unit_object + else: + strings_to_search = {unit: str(unit_object)} + + # Search for any matches that exist for that locale's unit. + # Needs to cycle all through strings as some locales have strings that + # could overlap in a regex match, since input validation isn't being performed. + for time_delta, time_string in strings_to_search.items(): + + # Replace {0} with regex \d representing digits + search_string = str(time_string) + search_string = search_string.format(r"\d+") + + # Create search pattern and find within string + pattern = re.compile(fr"{search_string}") + match = pattern.search(input_string) + + # If there is no match continue to next iteration + if not match: + continue + + match_string = match.group() + num_match = num_pattern.search(match_string) + + # If no number matches + # Need for absolute value as some locales have signs included in their objects + if not num_match: + change_value = ( + 1 if not time_delta.isnumeric() else abs(int(time_delta)) + ) + else: + change_value = int(num_match.group()) + + # No time to update if now is the unit + if unit == "now": + unit_visited[unit] = True + continue + + # Add change value to the correct unit (incorporates the plurality that exists within timeframe i.e second v.s seconds) + time_unit_to_change = str(unit) + time_unit_to_change += ( + "s" if (str(time_unit_to_change)[-1] != "s") else "" + ) + time_object_info[time_unit_to_change] = change_value + unit_visited[time_unit_to_change] = True + + # Assert error if string does not modify any units + if not any([True for k, v in unit_visited.items() if v]): + raise ValueError( + "Input string not valid. Note: Some locales do not support the week granulairty in Arrow. " + "If you are attempting to use the week granularity on an unsupported locale, this could be the cause of this error." + ) + + # Sign logic + future_string = locale_obj.future + future_string = future_string.format(".*") + future_pattern = re.compile(fr"^{future_string}$") + future_pattern_match = future_pattern.findall(input_string) + + past_string = locale_obj.past + past_string = past_string.format(".*") + past_pattern = re.compile(fr"^{past_string}$") + past_pattern_match = past_pattern.findall(input_string) + + # If a string contains the now unit, there will be no relative units, hence the need to check if the now unit + # was visited before raising a ValueError + if past_pattern_match: + sign_val = -1 + elif future_pattern_match: + sign_val = 1 + elif unit_visited["now"]: + sign_val = 0 else: - years = sign * int(max(delta / 31536000, 2)) - return locale.describe('years', years, only_distance=only_distance) + raise ValueError( + "Invalid input String. String does not contain any relative time information. " + "String should either represent a time in the future or a time in the past. " + "Ex: 'in 5 seconds' or '5 seconds ago'." + ) + time_changes = {k: sign_val * v for k, v in time_object_info.items()} + + return current_time.shift(**time_changes) + + # query functions + + def is_between( + self, + start: "Arrow", + end: "Arrow", + bounds: _BOUNDS = "()", + ) -> bool: + """Returns a boolean denoting whether the :class:`Arrow ` object is between + the start and end limits. + + :param start: an :class:`Arrow ` object. + :param end: an :class:`Arrow ` object. + :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies + whether to include or exclude the start and end values in the range. '(' excludes + the start, '[' includes the start, ')' excludes the end, and ']' includes the end. + If the bounds are not specified, the default bound '()' is used. + + Usage:: + + >>> start = arrow.get(datetime(2013, 5, 5, 12, 30, 10)) + >>> end = arrow.get(datetime(2013, 5, 5, 12, 30, 36)) + >>> arrow.get(datetime(2013, 5, 5, 12, 30, 27)).is_between(start, end) + True + + >>> start = arrow.get(datetime(2013, 5, 5)) + >>> end = arrow.get(datetime(2013, 5, 8)) + >>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[]') + True + + >>> start = arrow.get(datetime(2013, 5, 5)) + >>> end = arrow.get(datetime(2013, 5, 8)) + >>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[)') + False + + """ + + util.validate_bounds(bounds) + + if not isinstance(start, Arrow): + raise TypeError( + f"Cannot parse start date argument type of {type(start)!r}." + ) + + if not isinstance(end, Arrow): + raise TypeError(f"Cannot parse end date argument type of {type(start)!r}.") + + include_start = bounds[0] == "[" + include_end = bounds[1] == "]" + + target_ts = self.float_timestamp + start_ts = start.float_timestamp + end_ts = end.float_timestamp + + return ( + (start_ts <= target_ts <= end_ts) + and (include_start or start_ts < target_ts) + and (include_end or target_ts < end_ts) + ) + + # datetime methods + + def date(self) -> date: + """Returns a ``date`` object with the same year, month and day. + + Usage:: + + >>> arrow.utcnow().date() + datetime.date(2019, 1, 23) + + """ + + return self._datetime.date() + + def time(self) -> dt_time: + """Returns a ``time`` object with the same hour, minute, second, microsecond. + + Usage:: + + >>> arrow.utcnow().time() + datetime.time(12, 15, 34, 68352) + + """ + + return self._datetime.time() + + def timetz(self) -> dt_time: + """Returns a ``time`` object with the same hour, minute, second, microsecond and + tzinfo. + + Usage:: + + >>> arrow.utcnow().timetz() + datetime.time(12, 5, 18, 298893, tzinfo=tzutc()) + + """ + + return self._datetime.timetz() + + def astimezone(self, tz: Optional[dt_tzinfo]) -> dt_datetime: + """Returns a ``datetime`` object, converted to the specified timezone. + + :param tz: a ``tzinfo`` object. + + Usage:: + + >>> pacific=arrow.now('US/Pacific') + >>> nyc=arrow.now('America/New_York').tzinfo + >>> pacific.astimezone(nyc) + datetime.datetime(2019, 1, 20, 10, 24, 22, 328172, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York')) + + """ + + return self._datetime.astimezone(tz) + + def utcoffset(self) -> Optional[timedelta]: + """Returns a ``timedelta`` object representing the whole number of minutes difference from + UTC time. + + Usage:: + + >>> arrow.now('US/Pacific').utcoffset() + datetime.timedelta(-1, 57600) + + """ + + return self._datetime.utcoffset() + + def dst(self) -> Optional[timedelta]: + """Returns the daylight savings time adjustment. + + Usage:: + + >>> arrow.utcnow().dst() + datetime.timedelta(0) + + """ + + return self._datetime.dst() + + def timetuple(self) -> struct_time: + """Returns a ``time.struct_time``, in the current timezone. + + Usage:: + + >>> arrow.utcnow().timetuple() + time.struct_time(tm_year=2019, tm_mon=1, tm_mday=20, tm_hour=15, tm_min=17, tm_sec=8, tm_wday=6, tm_yday=20, tm_isdst=0) + + """ + + return self._datetime.timetuple() + + def utctimetuple(self) -> struct_time: + """Returns a ``time.struct_time``, in UTC time. + + Usage:: + + >>> arrow.utcnow().utctimetuple() + time.struct_time(tm_year=2019, tm_mon=1, tm_mday=19, tm_hour=21, tm_min=41, tm_sec=7, tm_wday=5, tm_yday=19, tm_isdst=0) + + """ + + return self._datetime.utctimetuple() + + def toordinal(self) -> int: + """Returns the proleptic Gregorian ordinal of the date. + + Usage:: + + >>> arrow.utcnow().toordinal() + 737078 + + """ + + return self._datetime.toordinal() + + def weekday(self) -> int: + """Returns the day of the week as an integer (0-6). + + Usage:: + + >>> arrow.utcnow().weekday() + 5 + + """ + + return self._datetime.weekday() + + def isoweekday(self) -> int: + """Returns the ISO day of the week as an integer (1-7). + + Usage:: + + >>> arrow.utcnow().isoweekday() + 6 + + """ + + return self._datetime.isoweekday() + + def isocalendar(self) -> Tuple[int, int, int]: + """Returns a 3-tuple, (ISO year, ISO week number, ISO weekday). + + Usage:: + + >>> arrow.utcnow().isocalendar() + (2019, 3, 6) + + """ + + return self._datetime.isocalendar() + + def isoformat(self, sep: str = "T", timespec: str = "auto") -> str: + """Returns an ISO 8601 formatted representation of the date and time. + + Usage:: + + >>> arrow.utcnow().isoformat() + '2019-01-19T18:30:52.442118+00:00' + + """ + + return self._datetime.isoformat(sep, timespec) + + def ctime(self) -> str: + """Returns a ctime formatted representation of the date and time. + + Usage:: + + >>> arrow.utcnow().ctime() + 'Sat Jan 19 18:26:50 2019' + + """ + + return self._datetime.ctime() + + def strftime(self, format: str) -> str: + """Formats in the style of ``datetime.strftime``. + + :param format: the format string. + + Usage:: + + >>> arrow.utcnow().strftime('%d-%m-%Y %H:%M:%S') + '23-01-2019 12:28:17' + + """ + + return self._datetime.strftime(format) + + def for_json(self) -> str: + """Serializes for the ``for_json`` protocol of simplejson. + + Usage:: + + >>> arrow.utcnow().for_json() + '2019-01-19T18:25:36.760079+00:00' + + """ + + return self.isoformat() # math - def __add__(self, other): + def __add__(self, other: Any) -> "Arrow": if isinstance(other, (timedelta, relativedelta)): return self.fromdatetime(self._datetime + other, self._datetime.tzinfo) - raise TypeError() + return NotImplemented - def __radd__(self, other): + def __radd__(self, other: Union[timedelta, relativedelta]) -> "Arrow": return self.__add__(other) - def __sub__(self, other): + @overload + def __sub__(self, other: Union[timedelta, relativedelta]) -> "Arrow": + pass # pragma: no cover + + @overload + def __sub__(self, other: Union[dt_datetime, "Arrow"]) -> timedelta: + pass # pragma: no cover + + def __sub__(self, other: Any) -> Union[timedelta, "Arrow"]: if isinstance(other, (timedelta, relativedelta)): return self.fromdatetime(self._datetime - other, self._datetime.tzinfo) - elif isinstance(other, datetime): + elif isinstance(other, dt_datetime): return self._datetime - other elif isinstance(other, Arrow): return self._datetime - other._datetime - raise TypeError() + return NotImplemented - def __rsub__(self, other): + def __rsub__(self, other: Any) -> timedelta: - if isinstance(other, datetime): + if isinstance(other, dt_datetime): return other - self._datetime - raise TypeError() - + return NotImplemented # comparisons - def _cmperror(self, other): - raise TypeError('can\'t compare \'{0}\' to \'{1}\''.format( - type(self), type(other))) + def __eq__(self, other: Any) -> bool: - def __eq__(self, other): - - if not isinstance(other, (Arrow, datetime)): + if not isinstance(other, (Arrow, dt_datetime)): return False return self._datetime == self._get_datetime(other) - def __ne__(self, other): + def __ne__(self, other: Any) -> bool: + + if not isinstance(other, (Arrow, dt_datetime)): + return True + return not self.__eq__(other) - def __gt__(self, other): + def __gt__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, datetime)): - self._cmperror(other) + if not isinstance(other, (Arrow, dt_datetime)): + return NotImplemented return self._datetime > self._get_datetime(other) - def __ge__(self, other): + def __ge__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, datetime)): - self._cmperror(other) + if not isinstance(other, (Arrow, dt_datetime)): + return NotImplemented return self._datetime >= self._get_datetime(other) - def __lt__(self, other): + def __lt__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, datetime)): - self._cmperror(other) + if not isinstance(other, (Arrow, dt_datetime)): + return NotImplemented return self._datetime < self._get_datetime(other) - def __le__(self, other): + def __le__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, datetime)): - self._cmperror(other) + if not isinstance(other, (Arrow, dt_datetime)): + return NotImplemented return self._datetime <= self._get_datetime(other) - - # datetime methods - - def date(self): - ''' Returns a ``date`` object with the same year, month and day. ''' - - return self._datetime.date() - - def time(self): - ''' Returns a ``time`` object with the same hour, minute, second, microsecond. ''' - - return self._datetime.time() - - def timetz(self): - ''' Returns a ``time`` object with the same hour, minute, second, microsecond and tzinfo. ''' - - return self._datetime.timetz() - - def astimezone(self, tz): - ''' Returns a ``datetime`` object, adjusted to the specified tzinfo. - - :param tz: a ``tzinfo`` object. - - ''' - - return self._datetime.astimezone(tz) - - def utcoffset(self): - ''' Returns a ``timedelta`` object representing the whole number of minutes difference from UTC time. ''' - - return self._datetime.utcoffset() - - def dst(self): - ''' Returns the daylight savings time adjustment. ''' - return self._datetime.dst() - - def timetuple(self): - ''' Returns a ``time.struct_time``, in the current timezone. ''' - - return self._datetime.timetuple() - - def utctimetuple(self): - ''' Returns a ``time.struct_time``, in UTC time. ''' - - return self._datetime.utctimetuple() - - def toordinal(self): - ''' Returns the proleptic Gregorian ordinal of the date. ''' - - return self._datetime.toordinal() - - def weekday(self): - ''' Returns the day of the week as an integer (0-6). ''' - - return self._datetime.weekday() - - def isoweekday(self): - ''' Returns the ISO day of the week as an integer (1-7). ''' - - return self._datetime.isoweekday() - - def isocalendar(self): - ''' Returns a 3-tuple, (ISO year, ISO week number, ISO weekday). ''' - - return self._datetime.isocalendar() - - def isoformat(self, sep='T'): - '''Returns an ISO 8601 formatted representation of the date and time. ''' - - return self._datetime.isoformat(sep) - - def ctime(self): - ''' Returns a ctime formatted representation of the date and time. ''' - - return self._datetime.ctime() - - def strftime(self, format): - ''' Formats in the style of ``datetime.strptime``. - - :param format: the format string. - - ''' - - return self._datetime.strftime(format) - - def for_json(self): - '''Serializes for the ``for_json`` protocol of simplejson.''' - return self.isoformat() - - # internal tools. - + # internal methods @staticmethod - def _get_tzinfo(tz_expr): - + def _get_tzinfo(tz_expr: Optional[TZ_EXPR]) -> dt_tzinfo: + """Get normalized tzinfo object from various inputs.""" if tz_expr is None: return dateutil_tz.tzutc() - if isinstance(tz_expr, tzinfo): + if isinstance(tz_expr, dt_tzinfo): return tz_expr else: try: return parser.TzinfoParser.parse(tz_expr) except parser.ParserError: - raise ValueError('\'{0}\' not recognized as a timezone'.format( - tz_expr)) + raise ValueError(f"{tz_expr!r} not recognized as a timezone.") @classmethod - def _get_datetime(cls, expr): - + def _get_datetime( + cls, expr: Union["Arrow", dt_datetime, int, float, str] + ) -> dt_datetime: + """Get datetime object from a specified expression.""" if isinstance(expr, Arrow): return expr.datetime - - if isinstance(expr, datetime): + elif isinstance(expr, dt_datetime): return expr - - try: - expr = float(expr) - return cls.utcfromtimestamp(expr).datetime - except: - raise ValueError( - '\'{0}\' not recognized as a timestamp or datetime'.format(expr)) + elif util.is_timestamp(expr): + timestamp = float(expr) + return cls.utcfromtimestamp(timestamp).datetime + else: + raise ValueError(f"{expr!r} not recognized as a datetime or timestamp.") @classmethod - def _get_frames(cls, name): + def _get_frames(cls, name: _T_FRAMES) -> Tuple[str, str, int]: + """Finds relevant timeframe and steps for use in range and span methods. + Returns a 3 element tuple in the form (frame, plural frame, step), for example ("day", "days", 1) + + """ if name in cls._ATTRS: - return name, '{0}s'.format(name), 1 - - elif name in ['week', 'weeks']: - return 'week', 'weeks', 1 - elif name in ['quarter', 'quarters']: - return 'quarter', 'months', 3 - - raise AttributeError() + return name, f"{name}s", 1 + elif name[-1] == "s" and name[:-1] in cls._ATTRS: + return name[:-1], name, 1 + elif name in ["week", "weeks"]: + return "week", "weeks", 1 + elif name in ["quarter", "quarters"]: + return "quarter", "months", 3 + else: + supported = ", ".join( + [ + "year(s)", + "month(s)", + "day(s)", + "hour(s)", + "minute(s)", + "second(s)", + "microsecond(s)", + "week(s)", + "quarter(s)", + ] + ) + raise ValueError( + f"Range or span over frame {name} not supported. Supported frames: {supported}." + ) @classmethod - def _get_iteration_params(cls, end, limit): - + def _get_iteration_params(cls, end: Any, limit: Optional[int]) -> Tuple[Any, int]: + """Sets default end and limit values for range method.""" if end is None: if limit is None: - raise Exception('one of \'end\' or \'limit\' is required') + raise ValueError("One of 'end' or 'limit' is required.") return cls.max, limit @@ -937,12 +1864,10 @@ class Arrow(object): return end, limit @staticmethod - def _get_timestamp_from_input(timestamp): + def _is_last_day_of_month(date: "Arrow") -> bool: + """Returns a boolean indicating whether the datetime is the last day of the month.""" + return date.day == calendar.monthrange(date.year, date.month)[1] - try: - return float(timestamp) - except: - raise ValueError('cannot parse \'{0}\' as a timestamp'.format(timestamp)) -Arrow.min = Arrow.fromdatetime(datetime.min) -Arrow.max = Arrow.fromdatetime(datetime.max) +Arrow.min = Arrow.fromdatetime(dt_datetime.min) +Arrow.max = Arrow.fromdatetime(dt_datetime.max) diff --git a/lib/arrow/constants.py b/lib/arrow/constants.py new file mode 100644 index 00000000..085ec392 --- /dev/null +++ b/lib/arrow/constants.py @@ -0,0 +1,146 @@ +"""Constants used internally in arrow.""" + +import sys +from datetime import datetime + +if sys.version_info < (3, 8): # pragma: no cover + from typing_extensions import Final +else: + from typing import Final # pragma: no cover + +# datetime.max.timestamp() errors on Windows, so we must hardcode +# the highest possible datetime value that can output a timestamp. +# tl;dr platform-independent max timestamps are hard to form +# See: https://stackoverflow.com/q/46133223 +try: + # Get max timestamp. Works on POSIX-based systems like Linux and macOS, + # but will trigger an OverflowError, ValueError, or OSError on Windows + _MAX_TIMESTAMP = datetime.max.timestamp() +except (OverflowError, ValueError, OSError): # pragma: no cover + # Fallback for Windows and 32-bit systems if initial max timestamp call fails + # Must get max value of ctime on Windows based on architecture (x32 vs x64) + # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/ctime-ctime32-ctime64-wctime-wctime32-wctime64 + # Note: this may occur on both 32-bit Linux systems (issue #930) along with Windows systems + is_64bits = sys.maxsize > 2 ** 32 + _MAX_TIMESTAMP = ( + datetime(3000, 1, 1, 23, 59, 59, 999999).timestamp() + if is_64bits + else datetime(2038, 1, 1, 23, 59, 59, 999999).timestamp() + ) + +MAX_TIMESTAMP: Final[float] = _MAX_TIMESTAMP +MAX_TIMESTAMP_MS: Final[float] = MAX_TIMESTAMP * 1000 +MAX_TIMESTAMP_US: Final[float] = MAX_TIMESTAMP * 1_000_000 + +MAX_ORDINAL: Final[int] = datetime.max.toordinal() +MIN_ORDINAL: Final[int] = 1 + +DEFAULT_LOCALE: Final[str] = "en-us" + +# Supported dehumanize locales +DEHUMANIZE_LOCALES = { + "en", + "en-us", + "en-gb", + "en-au", + "en-be", + "en-jp", + "en-za", + "en-ca", + "en-ph", + "fr", + "fr-fr", + "fr-ca", + "it", + "it-it", + "es", + "es-es", + "el", + "el-gr", + "ja", + "ja-jp", + "se", + "se-fi", + "se-no", + "se-se", + "sv", + "sv-se", + "fi", + "fi-fi", + "zh", + "zh-cn", + "zh-tw", + "zh-hk", + "nl", + "nl-nl", + "af", + "de", + "de-de", + "de-ch", + "de-at", + "nb", + "nb-no", + "nn", + "nn-no", + "pt", + "pt-pt", + "pt-br", + "tl", + "tl-ph", + "vi", + "vi-vn", + "tr", + "tr-tr", + "az", + "az-az", + "da", + "da-dk", + "ml", + "hi", + "fa", + "fa-ir", + "mr", + "ca", + "ca-es", + "ca-ad", + "ca-fr", + "ca-it", + "eo", + "eo-xx", + "bn", + "bn-bd", + "bn-in", + "rm", + "rm-ch", + "ro", + "ro-ro", + "sl", + "sl-si", + "id", + "id-id", + "ne", + "ne-np", + "ee", + "et", + "sw", + "sw-ke", + "sw-tz", + "la", + "la-va", + "lt", + "lt-lt", + "ms", + "ms-my", + "ms-bn", + "or", + "or-in", + "lb", + "lb-lu", + "zu", + "zu-za", + "sq", + "sq-al", + "ta", + "ta-in", + "ta-lk", +} diff --git a/lib/arrow/factory.py b/lib/arrow/factory.py index a5d690b2..aad4af8b 100644 --- a/lib/arrow/factory.py +++ b/lib/arrow/factory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Implements the :class:`ArrowFactory ` class, providing factory methods for common :class:`Arrow ` @@ -6,31 +5,100 @@ construction scenarios. """ -from __future__ import absolute_import -from arrow.arrow import Arrow -from arrow import parser -from arrow.util import is_timestamp, isstr - -from datetime import datetime, tzinfo, date -from dateutil import tz as dateutil_tz -from time import struct_time import calendar +from datetime import date, datetime +from datetime import tzinfo as dt_tzinfo +from decimal import Decimal +from time import struct_time +from typing import Any, List, Optional, Tuple, Type, Union, overload + +from dateutil import tz as dateutil_tz + +from arrow import parser +from arrow.arrow import TZ_EXPR, Arrow +from arrow.constants import DEFAULT_LOCALE +from arrow.util import is_timestamp, iso_to_gregorian -class ArrowFactory(object): - ''' A factory for generating :class:`Arrow ` objects. +class ArrowFactory: + """A factory for generating :class:`Arrow ` objects. :param type: (optional) the :class:`Arrow `-based class to construct from. Defaults to :class:`Arrow `. - ''' + """ - def __init__(self, type=Arrow): + type: Type[Arrow] + + def __init__(self, type: Type[Arrow] = Arrow) -> None: self.type = type - def get(self, *args, **kwargs): - ''' Returns an :class:`Arrow ` object based on flexible inputs. + @overload + def get( + self, + *, + locale: str = DEFAULT_LOCALE, + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, + ) -> Arrow: + ... # pragma: no cover + + @overload + def get( + self, + __obj: Union[ + Arrow, + datetime, + date, + struct_time, + dt_tzinfo, + int, + float, + str, + Tuple[int, int, int], + ], + *, + locale: str = DEFAULT_LOCALE, + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, + ) -> Arrow: + ... # pragma: no cover + + @overload + def get( + self, + __arg1: Union[datetime, date], + __arg2: TZ_EXPR, + *, + locale: str = DEFAULT_LOCALE, + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, + ) -> Arrow: + ... # pragma: no cover + + @overload + def get( + self, + __arg1: str, + __arg2: Union[str, List[str]], + *, + locale: str = DEFAULT_LOCALE, + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, + ) -> Arrow: + ... # pragma: no cover + + def get(self, *args: Any, **kwargs: Any) -> Arrow: + """Returns an :class:`Arrow ` object based on flexible inputs. + + :param locale: (optional) a ``str`` specifying a locale for the parser. Defaults to 'en-us'. + :param tzinfo: (optional) a :ref:`timezone expression ` or tzinfo object. + Replaces the timezone unless using an input form that is explicitly UTC or specifies + the timezone in a positional argument. Defaults to UTC. + :param normalize_whitespace: (optional) a ``bool`` specifying whether or not to normalize + redundant whitespace (spaces, tabs, and newlines) in a datetime string before parsing. + Defaults to false. Usage:: @@ -41,18 +109,14 @@ class ArrowFactory(object): >>> arrow.get() - **None** to also get current UTC time:: - - >>> arrow.get(None) - - **One** :class:`Arrow ` object, to get a copy. >>> arw = arrow.utcnow() >>> arrow.get(arw) - **One** ``str``, ``float``, or ``int``, convertible to a floating-point timestamp, to get that timestamp in UTC:: + **One** ``float`` or ``int``, convertible to a floating-point timestamp, to get + that timestamp in UTC:: >>> arrow.get(1367992474.293378) @@ -60,18 +124,17 @@ class ArrowFactory(object): >>> arrow.get(1367992474) - >>> arrow.get('1367992474.293378') - - - >>> arrow.get('1367992474') - - - **One** ISO-8601-formatted ``str``, to parse it:: + **One** ISO 8601-formatted ``str``, to parse it:: >>> arrow.get('2013-09-29T01:26:43.830580') - **One** ``tzinfo``, to get the current time in that timezone:: + **One** ISO 8601-formatted ``str``, in basic format, to parse it:: + + >>> arrow.get('20160413T133656.456289') + + + **One** ``tzinfo``, to get the current time **converted** to that timezone:: >>> arrow.get(tz.tzlocal()) @@ -91,85 +154,117 @@ class ArrowFactory(object): >>> arrow.get(date(2013, 5, 5)) - **Two** arguments, a naive or aware ``datetime``, and a timezone expression (as above):: + **One** time.struct time:: + + >>> arrow.get(gmtime(0)) + + + **One** iso calendar ``tuple``, to get that week date in UTC:: + + >>> arrow.get((2013, 18, 7)) + + + **Two** arguments, a naive or aware ``datetime``, and a replacement + :ref:`timezone expression `:: >>> arrow.get(datetime(2013, 5, 5), 'US/Pacific') - **Two** arguments, a naive ``date``, and a timezone expression (as above):: + **Two** arguments, a naive ``date``, and a replacement + :ref:`timezone expression `:: >>> arrow.get(date(2013, 5, 5), 'US/Pacific') **Two** arguments, both ``str``, to parse the first according to the format of the second:: - >>> arrow.get('2013-05-05 12:30:45', 'YYYY-MM-DD HH:mm:ss') - + >>> arrow.get('2013-05-05 12:30:45 America/Chicago', 'YYYY-MM-DD HH:mm:ss ZZZ') + **Two** arguments, first a ``str`` to parse and second a ``list`` of formats to try:: >>> arrow.get('2013-05-05 12:30:45', ['MM/DD/YYYY', 'YYYY-MM-DD HH:mm:ss']) - **Three or more** arguments, as for the constructor of a ``datetime``:: + **Three or more** arguments, as for the direct constructor of an ``Arrow`` object:: >>> arrow.get(2013, 5, 5, 12, 30, 45) - **One** time.struct time:: - >>> arrow.get(gmtime(0)) - - - ''' + """ arg_count = len(args) - locale = kwargs.get('locale', 'en_us') - tz = kwargs.get('tzinfo', None) + locale = kwargs.pop("locale", DEFAULT_LOCALE) + tz = kwargs.get("tzinfo", None) + normalize_whitespace = kwargs.pop("normalize_whitespace", False) - # () -> now, @ utc. + # if kwargs given, send to constructor unless only tzinfo provided + if len(kwargs) > 1: + arg_count = 3 + + # tzinfo kwarg is not provided + if len(kwargs) == 1 and tz is None: + arg_count = 3 + + # () -> now, @ tzinfo or utc if arg_count == 0: - if isinstance(tz, tzinfo): - return self.type.now(tz) + if isinstance(tz, str): + tz = parser.TzinfoParser.parse(tz) + return self.type.now(tzinfo=tz) + + if isinstance(tz, dt_tzinfo): + return self.type.now(tzinfo=tz) + return self.type.utcnow() if arg_count == 1: arg = args[0] + if isinstance(arg, Decimal): + arg = float(arg) - # (None) -> now, @ utc. + # (None) -> raises an exception if arg is None: - return self.type.utcnow() + raise TypeError("Cannot parse argument of type None.") - # try (int, float, str(int), str(float)) -> utc, from timestamp. - if is_timestamp(arg): - return self.type.utcfromtimestamp(arg) + # try (int, float) -> from timestamp @ tzinfo + elif not isinstance(arg, str) and is_timestamp(arg): + if tz is None: + # set to UTC by default + tz = dateutil_tz.tzutc() + return self.type.fromtimestamp(arg, tzinfo=tz) - # (Arrow) -> from the object's datetime. - if isinstance(arg, Arrow): - return self.type.fromdatetime(arg.datetime) + # (Arrow) -> from the object's datetime @ tzinfo + elif isinstance(arg, Arrow): + return self.type.fromdatetime(arg.datetime, tzinfo=tz) - # (datetime) -> from datetime. - if isinstance(arg, datetime): - return self.type.fromdatetime(arg) + # (datetime) -> from datetime @ tzinfo + elif isinstance(arg, datetime): + return self.type.fromdatetime(arg, tzinfo=tz) - # (date) -> from date. - if isinstance(arg, date): - return self.type.fromdate(arg) + # (date) -> from date @ tzinfo + elif isinstance(arg, date): + return self.type.fromdate(arg, tzinfo=tz) - # (tzinfo) -> now, @ tzinfo. - elif isinstance(arg, tzinfo): - return self.type.now(arg) + # (tzinfo) -> now @ tzinfo + elif isinstance(arg, dt_tzinfo): + return self.type.now(tzinfo=arg) - # (str) -> now, @ tzinfo. - elif isstr(arg): - dt = parser.DateTimeParser(locale).parse_iso(arg) - return self.type.fromdatetime(dt) + # (str) -> parse @ tzinfo + elif isinstance(arg, str): + dt = parser.DateTimeParser(locale).parse_iso(arg, normalize_whitespace) + return self.type.fromdatetime(dt, tzinfo=tz) # (struct_time) -> from struct_time elif isinstance(arg, struct_time): return self.type.utcfromtimestamp(calendar.timegm(arg)) + # (iso calendar) -> convert then from date @ tzinfo + elif isinstance(arg, tuple) and len(arg) == 3: + d = iso_to_gregorian(*arg) + return self.type.fromdate(d, tzinfo=tz) + else: - raise TypeError('Can\'t parse single argument type of \'{0}\''.format(type(arg))) + raise TypeError(f"Cannot parse single argument of type {type(arg)!r}.") elif arg_count == 2: @@ -177,58 +272,57 @@ class ArrowFactory(object): if isinstance(arg_1, datetime): - # (datetime, tzinfo) -> fromdatetime @ tzinfo/string. - if isinstance(arg_2, tzinfo) or isstr(arg_2): - return self.type.fromdatetime(arg_1, arg_2) + # (datetime, tzinfo/str) -> fromdatetime @ tzinfo + if isinstance(arg_2, (dt_tzinfo, str)): + return self.type.fromdatetime(arg_1, tzinfo=arg_2) else: - raise TypeError('Can\'t parse two arguments of types \'datetime\', \'{0}\''.format( - type(arg_2))) + raise TypeError( + f"Cannot parse two arguments of types 'datetime', {type(arg_2)!r}." + ) - # (date, tzinfo/str) -> fromdate @ tzinfo/string. elif isinstance(arg_1, date): - if isinstance(arg_2, tzinfo) or isstr(arg_2): + # (date, tzinfo/str) -> fromdate @ tzinfo + if isinstance(arg_2, (dt_tzinfo, str)): return self.type.fromdate(arg_1, tzinfo=arg_2) else: - raise TypeError('Can\'t parse two arguments of types \'date\', \'{0}\''.format( - type(arg_2))) + raise TypeError( + f"Cannot parse two arguments of types 'date', {type(arg_2)!r}." + ) - # (str, format) -> parse. - elif isstr(arg_1) and (isstr(arg_2) or isinstance(arg_2, list)): - dt = parser.DateTimeParser(locale).parse(args[0], args[1]) + # (str, format) -> parse @ tzinfo + elif isinstance(arg_1, str) and isinstance(arg_2, (str, list)): + dt = parser.DateTimeParser(locale).parse( + args[0], args[1], normalize_whitespace + ) return self.type.fromdatetime(dt, tzinfo=tz) else: - raise TypeError('Can\'t parse two arguments of types \'{0}\', \'{1}\''.format( - type(arg_1), type(arg_2))) + raise TypeError( + f"Cannot parse two arguments of types {type(arg_1)!r} and {type(arg_2)!r}." + ) - # 3+ args -> datetime-like via constructor. + # 3+ args -> datetime-like via constructor else: return self.type(*args, **kwargs) - def utcnow(self): - '''Returns an :class:`Arrow ` object, representing "now" in UTC time. + def utcnow(self) -> Arrow: + """Returns an :class:`Arrow ` object, representing "now" in UTC time. Usage:: >>> import arrow >>> arrow.utcnow() - ''' + """ return self.type.utcnow() - def now(self, tz=None): - '''Returns an :class:`Arrow ` object, representing "now". + def now(self, tz: Optional[TZ_EXPR] = None) -> Arrow: + """Returns an :class:`Arrow ` object, representing "now" in the given + timezone. - :param tz: (optional) An expression representing a timezone. Defaults to local time. - - Recognized timezone expressions: - - - A ``tzinfo`` object. - - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. - - A ``str`` in ISO-8601 style, as in '+07:00'. - - A ``str``, one of the following: 'local', 'utc', 'UTC'. + :param tz: (optional) A :ref:`timezone expression `. Defaults to local time. Usage:: @@ -244,11 +338,11 @@ class ArrowFactory(object): >>> arrow.now('local') - ''' + """ if tz is None: tz = dateutil_tz.tzlocal() - elif not isinstance(tz, tzinfo): + elif not isinstance(tz, dt_tzinfo): tz = parser.TzinfoParser.parse(tz) return self.type.now(tz) diff --git a/lib/arrow/formatter.py b/lib/arrow/formatter.py index 50fd3a17..728bea1a 100644 --- a/lib/arrow/formatter.py +++ b/lib/arrow/formatter.py @@ -1,105 +1,152 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import +"""Provides the :class:`Arrow ` class, an improved formatter for datetimes.""" -import calendar import re +import sys +from datetime import datetime, timedelta +from typing import Optional, Pattern, cast + from dateutil import tz as dateutil_tz -from arrow import util, locales + +from arrow import locales +from arrow.constants import DEFAULT_LOCALE + +if sys.version_info < (3, 8): # pragma: no cover + from typing_extensions import Final +else: + from typing import Final # pragma: no cover -class DateTimeFormatter(object): +FORMAT_ATOM: Final[str] = "YYYY-MM-DD HH:mm:ssZZ" +FORMAT_COOKIE: Final[str] = "dddd, DD-MMM-YYYY HH:mm:ss ZZZ" +FORMAT_RFC822: Final[str] = "ddd, DD MMM YY HH:mm:ss Z" +FORMAT_RFC850: Final[str] = "dddd, DD-MMM-YY HH:mm:ss ZZZ" +FORMAT_RFC1036: Final[str] = "ddd, DD MMM YY HH:mm:ss Z" +FORMAT_RFC1123: Final[str] = "ddd, DD MMM YYYY HH:mm:ss Z" +FORMAT_RFC2822: Final[str] = "ddd, DD MMM YYYY HH:mm:ss Z" +FORMAT_RFC3339: Final[str] = "YYYY-MM-DD HH:mm:ssZZ" +FORMAT_RSS: Final[str] = "ddd, DD MMM YYYY HH:mm:ss Z" +FORMAT_W3C: Final[str] = "YYYY-MM-DD HH:mm:ssZZ" - _FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?|a|A|X)') - def __init__(self, locale='en_us'): +class DateTimeFormatter: + + # This pattern matches characters enclosed in square brackets are matched as + # an atomic group. For more info on atomic groups and how to they are + # emulated in Python's re library, see https://stackoverflow.com/a/13577411/2701578 + + _FORMAT_RE: Final[Pattern[str]] = re.compile( + r"(\[(?:(?=(?P[^]]))(?P=literal))*\]|YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X|x|W)" + ) + + locale: locales.Locale + + def __init__(self, locale: str = DEFAULT_LOCALE) -> None: self.locale = locales.get_locale(locale) - def format(cls, dt, fmt): + def format(cls, dt: datetime, fmt: str) -> str: - return cls._FORMAT_RE.sub(lambda m: cls._format_token(dt, m.group(0)), fmt) + # FIXME: _format_token() is nullable + return cls._FORMAT_RE.sub( + lambda m: cast(str, cls._format_token(dt, m.group(0))), fmt + ) - def _format_token(self, dt, token): + def _format_token(self, dt: datetime, token: Optional[str]) -> Optional[str]: - if token == 'YYYY': + if token and token.startswith("[") and token.endswith("]"): + return token[1:-1] + + if token == "YYYY": return self.locale.year_full(dt.year) - if token == 'YY': + if token == "YY": return self.locale.year_abbreviation(dt.year) - if token == 'MMMM': + if token == "MMMM": return self.locale.month_name(dt.month) - if token == 'MMM': + if token == "MMM": return self.locale.month_abbreviation(dt.month) - if token == 'MM': - return '{0:02d}'.format(dt.month) - if token == 'M': - return str(dt.month) + if token == "MM": + return f"{dt.month:02d}" + if token == "M": + return f"{dt.month}" - if token == 'DDDD': - return '{0:03d}'.format(dt.timetuple().tm_yday) - if token == 'DDD': - return str(dt.timetuple().tm_yday) - if token == 'DD': - return '{0:02d}'.format(dt.day) - if token == 'D': - return str(dt.day) + if token == "DDDD": + return f"{dt.timetuple().tm_yday:03d}" + if token == "DDD": + return f"{dt.timetuple().tm_yday}" + if token == "DD": + return f"{dt.day:02d}" + if token == "D": + return f"{dt.day}" - if token == 'Do': + if token == "Do": return self.locale.ordinal_number(dt.day) - if token == 'dddd': + if token == "dddd": return self.locale.day_name(dt.isoweekday()) - if token == 'ddd': + if token == "ddd": return self.locale.day_abbreviation(dt.isoweekday()) - if token == 'd': - return str(dt.isoweekday()) + if token == "d": + return f"{dt.isoweekday()}" - if token == 'HH': - return '{0:02d}'.format(dt.hour) - if token == 'H': - return str(dt.hour) - if token == 'hh': - return '{0:02d}'.format(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) - if token == 'h': - return str(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) + if token == "HH": + return f"{dt.hour:02d}" + if token == "H": + return f"{dt.hour}" + if token == "hh": + return f"{dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12):02d}" + if token == "h": + return f"{dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)}" - if token == 'mm': - return '{0:02d}'.format(dt.minute) - if token == 'm': - return str(dt.minute) + if token == "mm": + return f"{dt.minute:02d}" + if token == "m": + return f"{dt.minute}" - if token == 'ss': - return '{0:02d}'.format(dt.second) - if token == 's': - return str(dt.second) + if token == "ss": + return f"{dt.second:02d}" + if token == "s": + return f"{dt.second}" - if token == 'SSSSSS': - return str('{0:06d}'.format(int(dt.microsecond))) - if token == 'SSSSS': - return str('{0:05d}'.format(int(dt.microsecond / 10))) - if token == 'SSSS': - return str('{0:04d}'.format(int(dt.microsecond / 100))) - if token == 'SSS': - return str('{0:03d}'.format(int(dt.microsecond / 1000))) - if token == 'SS': - return str('{0:02d}'.format(int(dt.microsecond / 10000))) - if token == 'S': - return str(int(dt.microsecond / 100000)) + if token == "SSSSSS": + return f"{dt.microsecond:06d}" + if token == "SSSSS": + return f"{dt.microsecond // 10:05d}" + if token == "SSSS": + return f"{dt.microsecond // 100:04d}" + if token == "SSS": + return f"{dt.microsecond // 1000:03d}" + if token == "SS": + return f"{dt.microsecond // 10000:02d}" + if token == "S": + return f"{dt.microsecond // 100000}" - if token == 'X': - return str(calendar.timegm(dt.utctimetuple())) + if token == "X": + return f"{dt.timestamp()}" - if token in ['ZZ', 'Z']: - separator = ':' if token == 'ZZ' else '' + if token == "x": + return f"{dt.timestamp() * 1_000_000:.0f}" + + if token == "ZZZ": + return dt.tzname() + + if token in ["ZZ", "Z"]: + separator = ":" if token == "ZZ" else "" tz = dateutil_tz.tzutc() if dt.tzinfo is None else dt.tzinfo - total_minutes = int(util.total_seconds(tz.utcoffset(dt)) / 60) + # `dt` must be aware object. Otherwise, this line will raise AttributeError + # https://github.com/arrow-py/arrow/pull/883#discussion_r529866834 + # datetime awareness: https://docs.python.org/3/library/datetime.html#aware-and-naive-objects + total_minutes = int(cast(timedelta, tz.utcoffset(dt)).total_seconds() / 60) - sign = '+' if total_minutes >= 0 else '-' + sign = "+" if total_minutes >= 0 else "-" total_minutes = abs(total_minutes) hour, minute = divmod(total_minutes, 60) - return '{0}{1:02d}{2}{3:02d}'.format(sign, hour, separator, minute) + return f"{sign}{hour:02d}{separator}{minute:02d}" - if token in ('a', 'A'): + if token in ("a", "A"): return self.locale.meridian(dt.hour, token) + if token == "W": + year, week, day = dt.isocalendar() + return f"{year}-W{week:02d}-{day}" diff --git a/lib/arrow/locales.py b/lib/arrow/locales.py index 7cf7f4c3..6221df7a 100644 --- a/lib/arrow/locales.py +++ b/lib/arrow/locales.py @@ -1,180 +1,280 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals +"""Provides internationalization for arrow in over 60 languages and dialects.""" -import inspect import sys +from math import trunc +from typing import ( + Any, + ClassVar, + Dict, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + Union, + cast, +) + +if sys.version_info < (3, 8): # pragma: no cover + from typing_extensions import Literal +else: + from typing import Literal # pragma: no cover + +TimeFrameLiteral = Literal[ + "now", + "second", + "seconds", + "minute", + "minutes", + "hour", + "hours", + "day", + "days", + "week", + "weeks", + "month", + "months", + "year", + "years", +] + +_TimeFrameElements = Union[ + str, Sequence[str], Mapping[str, str], Mapping[str, Sequence[str]] +] -def get_locale(name): - '''Returns an appropriate :class:`Locale ` - corresponding to an inpute locale name. +_locale_map: Dict[str, Type["Locale"]] = dict() + + +def get_locale(name: str) -> "Locale": + """Returns an appropriate :class:`Locale ` + corresponding to an input locale name. :param name: the name of the locale. - ''' + """ - locale_cls = _locales.get(name.lower()) + normalized_locale_name = name.lower().replace("_", "-") + locale_cls = _locale_map.get(normalized_locale_name) if locale_cls is None: - raise ValueError('Unsupported locale \'{0}\''.format(name)) + raise ValueError(f"Unsupported locale {normalized_locale_name!r}.") return locale_cls() -# base locale type. +def get_locale_by_class_name(name: str) -> "Locale": + """Returns an appropriate :class:`Locale ` + corresponding to an locale class name. -class Locale(object): - ''' Represents locale-specific data and functionality. ''' + :param name: the name of the locale class. - names = [] + """ + locale_cls: Optional[Type[Locale]] = globals().get(name) - timeframes = { - 'now': '', - 'seconds': '', - 'minute': '', - 'minutes': '', - 'hour': '', - 'hours': '', - 'day': '', - 'days': '', - 'month': '', - 'months': '', - 'year': '', - 'years': '', + if locale_cls is None: + raise ValueError(f"Unsupported locale {name!r}.") + + return locale_cls() + + +class Locale: + """Represents locale-specific data and functionality.""" + + names: ClassVar[List[str]] = [] + + timeframes: ClassVar[Mapping[TimeFrameLiteral, _TimeFrameElements]] = { + "now": "", + "second": "", + "seconds": "", + "minute": "", + "minutes": "", + "hour": "", + "hours": "", + "day": "", + "days": "", + "week": "", + "weeks": "", + "month": "", + "months": "", + "year": "", + "years": "", } - meridians = { - 'am': '', - 'pm': '', - 'AM': '', - 'PM': '', - } + meridians: ClassVar[Dict[str, str]] = {"am": "", "pm": "", "AM": "", "PM": ""} - past = None - future = None + past: ClassVar[str] + future: ClassVar[str] + and_word: ClassVar[Optional[str]] = None - month_names = [] - month_abbreviations = [] + month_names: ClassVar[List[str]] = [] + month_abbreviations: ClassVar[List[str]] = [] - day_names = [] - day_abbreviations = [] + day_names: ClassVar[List[str]] = [] + day_abbreviations: ClassVar[List[str]] = [] - ordinal_day_re = r'(\d+)' + ordinal_day_re: ClassVar[str] = r"(\d+)" - def __init__(self): + _month_name_to_ordinal: Optional[Dict[str, int]] + + def __init_subclass__(cls, **kwargs: Any) -> None: + for locale_name in cls.names: + if locale_name in _locale_map: + raise LookupError(f"Duplicated locale name: {locale_name}") + + _locale_map[locale_name.lower().replace("_", "-")] = cls + + def __init__(self) -> None: self._month_name_to_ordinal = None - def describe(self, timeframe, delta=0, only_distance=False): - ''' Describes a delta within a timeframe in plain language. + def describe( + self, + timeframe: TimeFrameLiteral, + delta: Union[float, int] = 0, + only_distance: bool = False, + ) -> str: + """Describes a delta within a timeframe in plain language. :param timeframe: a string representing a timeframe. :param delta: a quantity representing a delta in a timeframe. :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords - ''' + """ - humanized = self._format_timeframe(timeframe, delta) + humanized = self._format_timeframe(timeframe, trunc(delta)) if not only_distance: humanized = self._format_relative(humanized, timeframe, delta) return humanized - def day_name(self, day): - ''' Returns the day name for a specified day of the week. + def describe_multi( + self, + timeframes: Sequence[Tuple[TimeFrameLiteral, Union[int, float]]], + only_distance: bool = False, + ) -> str: + """Describes a delta within multiple timeframes in plain language. + + :param timeframes: a list of string, quantity pairs each representing a timeframe and delta. + :param only_distance: return only distance eg: "2 hours and 11 seconds" without "in" or "ago" keywords + """ + + parts = [ + self._format_timeframe(timeframe, trunc(delta)) + for timeframe, delta in timeframes + ] + if self.and_word: + parts.insert(-1, self.and_word) + humanized = " ".join(parts) + + if not only_distance: + humanized = self._format_relative(humanized, *timeframes[-1]) + + return humanized + + def day_name(self, day: int) -> str: + """Returns the day name for a specified day of the week. :param day: the ``int`` day of the week (1-7). - ''' + """ return self.day_names[day] - def day_abbreviation(self, day): - ''' Returns the day abbreviation for a specified day of the week. + def day_abbreviation(self, day: int) -> str: + """Returns the day abbreviation for a specified day of the week. :param day: the ``int`` day of the week (1-7). - ''' + """ return self.day_abbreviations[day] - def month_name(self, month): - ''' Returns the month name for a specified month of the year. + def month_name(self, month: int) -> str: + """Returns the month name for a specified month of the year. :param month: the ``int`` month of the year (1-12). - ''' + """ return self.month_names[month] - def month_abbreviation(self, month): - ''' Returns the month abbreviation for a specified month of the year. + def month_abbreviation(self, month: int) -> str: + """Returns the month abbreviation for a specified month of the year. :param month: the ``int`` month of the year (1-12). - ''' + """ return self.month_abbreviations[month] - def month_number(self, name): - ''' Returns the month number for a month specified by name or abbreviation. + def month_number(self, name: str) -> Optional[int]: + """Returns the month number for a month specified by name or abbreviation. :param name: the month name or abbreviation. - ''' + """ if self._month_name_to_ordinal is None: self._month_name_to_ordinal = self._name_to_ordinal(self.month_names) - self._month_name_to_ordinal.update(self._name_to_ordinal(self.month_abbreviations)) + self._month_name_to_ordinal.update( + self._name_to_ordinal(self.month_abbreviations) + ) return self._month_name_to_ordinal.get(name) - def year_full(self, year): - ''' Returns the year for specific locale if available + def year_full(self, year: int) -> str: + """Returns the year for specific locale if available - :param name: the ``int`` year (4-digit) - ''' - return '{0:04d}'.format(year) + :param year: the ``int`` year (4-digit) + """ + return f"{year:04d}" - def year_abbreviation(self, year): - ''' Returns the year for specific locale if available + def year_abbreviation(self, year: int) -> str: + """Returns the year for specific locale if available - :param name: the ``int`` year (4-digit) - ''' - return '{0:04d}'.format(year)[2:] + :param year: the ``int`` year (4-digit) + """ + return f"{year:04d}"[2:] - def meridian(self, hour, token): - ''' Returns the meridian indicator for a specified hour and format token. + def meridian(self, hour: int, token: Any) -> Optional[str]: + """Returns the meridian indicator for a specified hour and format token. :param hour: the ``int`` hour of the day. :param token: the format token. - ''' + """ - if token == 'a': - return self.meridians['am'] if hour < 12 else self.meridians['pm'] - if token == 'A': - return self.meridians['AM'] if hour < 12 else self.meridians['PM'] + if token == "a": + return self.meridians["am"] if hour < 12 else self.meridians["pm"] + if token == "A": + return self.meridians["AM"] if hour < 12 else self.meridians["PM"] + return None - def ordinal_number(self, n): - ''' Returns the ordinal format of a given integer + def ordinal_number(self, n: int) -> str: + """Returns the ordinal format of a given integer :param n: an integer - ''' + """ return self._ordinal_number(n) - def _ordinal_number(self, n): - return '{0}'.format(n) + def _ordinal_number(self, n: int) -> str: + return f"{n}" - def _name_to_ordinal(self, lst): - return dict(map(lambda i: (i[1].lower(), i[0] + 1), enumerate(lst[1:]))) + def _name_to_ordinal(self, lst: Sequence[str]) -> Dict[str, int]: + return {elem.lower(): i for i, elem in enumerate(lst[1:], 1)} - def _format_timeframe(self, timeframe, delta): + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + # TODO: remove cast + return cast(str, self.timeframes[timeframe]).format(trunc(abs(delta))) - return self.timeframes[timeframe].format(abs(delta)) + def _format_relative( + self, + humanized: str, + timeframe: TimeFrameLiteral, + delta: Union[float, int], + ) -> str: - def _format_relative(self, humanized, timeframe, delta): - - if timeframe == 'now': + if timeframe == "now": return humanized direction = self.past if delta < 0 else self.future @@ -182,1830 +282,5365 @@ class Locale(object): return direction.format(humanized) -# base locale type implementations. - class EnglishLocale(Locale): - names = ['en', 'en_us', 'en_gb', 'en_au', 'en_be', 'en_jp', 'en_za', 'en_ca'] + names = [ + "en", + "en-us", + "en-gb", + "en-au", + "en-be", + "en-jp", + "en-za", + "en-ca", + "en-ph", + ] - past = '{0} ago' - future = 'in {0}' + past = "{0} ago" + future = "in {0}" + and_word = "and" timeframes = { - 'now': 'just now', - 'seconds': 'seconds', - 'minute': 'a minute', - 'minutes': '{0} minutes', - 'hour': 'an hour', - 'hours': '{0} hours', - 'day': 'a day', - 'days': '{0} days', - 'month': 'a month', - 'months': '{0} months', - 'year': 'a year', - 'years': '{0} years', + "now": "just now", + "second": "a second", + "seconds": "{0} seconds", + "minute": "a minute", + "minutes": "{0} minutes", + "hour": "an hour", + "hours": "{0} hours", + "day": "a day", + "days": "{0} days", + "week": "a week", + "weeks": "{0} weeks", + "month": "a month", + "months": "{0} months", + "year": "a year", + "years": "{0} years", } - meridians = { - 'am': 'am', - 'pm': 'pm', - 'AM': 'AM', - 'PM': 'PM', - } + meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"} - month_names = ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', - 'August', 'September', 'October', 'November', 'December'] - month_abbreviations = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', - 'Sep', 'Oct', 'Nov', 'Dec'] + month_names = [ + "", + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ] + month_abbreviations = [ + "", + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ] - day_names = ['', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] - day_abbreviations = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + day_names = [ + "", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ] + day_abbreviations = ["", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - ordinal_day_re = r'((?P[2-3]?1(?=st)|[2-3]?2(?=nd)|[2-3]?3(?=rd)|[1-3]?[04-9](?=th)|1[1-3](?=th))(st|nd|rd|th))' + ordinal_day_re = r"((?P[2-3]?1(?=st)|[2-3]?2(?=nd)|[2-3]?3(?=rd)|[1-3]?[04-9](?=th)|1[1-3](?=th))(st|nd|rd|th))" - def _ordinal_number(self, n): + def _ordinal_number(self, n: int) -> str: if n % 100 not in (11, 12, 13): remainder = abs(n) % 10 if remainder == 1: - return '{0}st'.format(n) + return f"{n}st" elif remainder == 2: - return '{0}nd'.format(n) + return f"{n}nd" elif remainder == 3: - return '{0}rd'.format(n) - return '{0}th'.format(n) + return f"{n}rd" + return f"{n}th" + + def describe( + self, + timeframe: TimeFrameLiteral, + delta: Union[int, float] = 0, + only_distance: bool = False, + ) -> str: + """Describes a delta within a timeframe in plain language. + + :param timeframe: a string representing a timeframe. + :param delta: a quantity representing a delta in a timeframe. + :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords + """ + + humanized = super().describe(timeframe, delta, only_distance) + if only_distance and timeframe == "now": + humanized = "instantly" + + return humanized class ItalianLocale(Locale): - names = ['it', 'it_it'] - past = '{0} fa' - future = 'tra {0}' + names = ["it", "it-it"] + past = "{0} fa" + future = "tra {0}" + and_word = "e" timeframes = { - 'now': 'adesso', - 'seconds': 'qualche secondo', - 'minute': 'un minuto', - 'minutes': '{0} minuti', - 'hour': 'un\'ora', - 'hours': '{0} ore', - 'day': 'un giorno', - 'days': '{0} giorni', - 'month': 'un mese', - 'months': '{0} mesi', - 'year': 'un anno', - 'years': '{0} anni', + "now": "adesso", + "second": "un secondo", + "seconds": "{0} qualche secondo", + "minute": "un minuto", + "minutes": "{0} minuti", + "hour": "un'ora", + "hours": "{0} ore", + "day": "un giorno", + "days": "{0} giorni", + "week": "una settimana,", + "weeks": "{0} settimane", + "month": "un mese", + "months": "{0} mesi", + "year": "un anno", + "years": "{0} anni", } - month_names = ['', 'gennaio', 'febbraio', 'marzo', 'aprile', 'maggio', 'giugno', 'luglio', - 'agosto', 'settembre', 'ottobre', 'novembre', 'dicembre'] - month_abbreviations = ['', 'gen', 'feb', 'mar', 'apr', 'mag', 'giu', 'lug', 'ago', - 'set', 'ott', 'nov', 'dic'] + month_names = [ + "", + "gennaio", + "febbraio", + "marzo", + "aprile", + "maggio", + "giugno", + "luglio", + "agosto", + "settembre", + "ottobre", + "novembre", + "dicembre", + ] + month_abbreviations = [ + "", + "gen", + "feb", + "mar", + "apr", + "mag", + "giu", + "lug", + "ago", + "set", + "ott", + "nov", + "dic", + ] - day_names = ['', 'lunedì', 'martedì', 'mercoledì', 'giovedì', 'venerdì', 'sabato', 'domenica'] - day_abbreviations = ['', 'lun', 'mar', 'mer', 'gio', 'ven', 'sab', 'dom'] + day_names = [ + "", + "lunedì", + "martedì", + "mercoledì", + "giovedì", + "venerdì", + "sabato", + "domenica", + ] + day_abbreviations = ["", "lun", "mar", "mer", "gio", "ven", "sab", "dom"] - ordinal_day_re = r'((?P[1-3]?[0-9](?=[ºª]))[ºª])' + ordinal_day_re = r"((?P[1-3]?[0-9](?=[ºª]))[ºª])" - def _ordinal_number(self, n): - return '{0}º'.format(n) + def _ordinal_number(self, n: int) -> str: + return f"{n}º" class SpanishLocale(Locale): - names = ['es', 'es_es'] - past = 'hace {0}' - future = 'en {0}' + names = ["es", "es-es"] + past = "hace {0}" + future = "en {0}" + and_word = "y" timeframes = { - 'now': 'ahora', - 'seconds': 'segundos', - 'minute': 'un minuto', - 'minutes': '{0} minutos', - 'hour': 'una hora', - 'hours': '{0} horas', - 'day': 'un día', - 'days': '{0} días', - 'month': 'un mes', - 'months': '{0} meses', - 'year': 'un año', - 'years': '{0} años', + "now": "ahora", + "second": "un segundo", + "seconds": "{0} segundos", + "minute": "un minuto", + "minutes": "{0} minutos", + "hour": "una hora", + "hours": "{0} horas", + "day": "un día", + "days": "{0} días", + "week": "una semana", + "weeks": "{0} semanas", + "month": "un mes", + "months": "{0} meses", + "year": "un año", + "years": "{0} años", } - month_names = ['', 'enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', - 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'] - month_abbreviations = ['', 'ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', - 'sep', 'oct', 'nov', 'dic'] + meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"} - day_names = ['', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado', 'domingo'] - day_abbreviations = ['', 'lun', 'mar', 'mie', 'jue', 'vie', 'sab', 'dom'] + month_names = [ + "", + "enero", + "febrero", + "marzo", + "abril", + "mayo", + "junio", + "julio", + "agosto", + "septiembre", + "octubre", + "noviembre", + "diciembre", + ] + month_abbreviations = [ + "", + "ene", + "feb", + "mar", + "abr", + "may", + "jun", + "jul", + "ago", + "sep", + "oct", + "nov", + "dic", + ] - ordinal_day_re = r'((?P[1-3]?[0-9](?=[ºª]))[ºª])' + day_names = [ + "", + "lunes", + "martes", + "miércoles", + "jueves", + "viernes", + "sábado", + "domingo", + ] + day_abbreviations = ["", "lun", "mar", "mie", "jue", "vie", "sab", "dom"] - def _ordinal_number(self, n): - return '{0}º'.format(n) + ordinal_day_re = r"((?P[1-3]?[0-9](?=[ºª]))[ºª])" + + def _ordinal_number(self, n: int) -> str: + return f"{n}º" -class FrenchLocale(Locale): - names = ['fr', 'fr_fr'] - past = 'il y a {0}' - future = 'dans {0}' +class FrenchBaseLocale(Locale): + + past = "il y a {0}" + future = "dans {0}" + and_word = "et" timeframes = { - 'now': 'maintenant', - 'seconds': 'quelques secondes', - 'minute': 'une minute', - 'minutes': '{0} minutes', - 'hour': 'une heure', - 'hours': '{0} heures', - 'day': 'un jour', - 'days': '{0} jours', - 'month': 'un mois', - 'months': '{0} mois', - 'year': 'un an', - 'years': '{0} ans', + "now": "maintenant", + "second": "une seconde", + "seconds": "{0} secondes", + "minute": "une minute", + "minutes": "{0} minutes", + "hour": "une heure", + "hours": "{0} heures", + "day": "un jour", + "days": "{0} jours", + "week": "une semaine", + "weeks": "{0} semaines", + "month": "un mois", + "months": "{0} mois", + "year": "un an", + "years": "{0} ans", } - month_names = ['', 'janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', - 'août', 'septembre', 'octobre', 'novembre', 'décembre'] - month_abbreviations = ['', 'janv', 'févr', 'mars', 'avr', 'mai', 'juin', 'juil', 'août', - 'sept', 'oct', 'nov', 'déc'] + month_names = [ + "", + "janvier", + "février", + "mars", + "avril", + "mai", + "juin", + "juillet", + "août", + "septembre", + "octobre", + "novembre", + "décembre", + ] - day_names = ['', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche'] - day_abbreviations = ['', 'lun', 'mar', 'mer', 'jeu', 'ven', 'sam', 'dim'] + day_names = [ + "", + "lundi", + "mardi", + "mercredi", + "jeudi", + "vendredi", + "samedi", + "dimanche", + ] + day_abbreviations = ["", "lun", "mar", "mer", "jeu", "ven", "sam", "dim"] - ordinal_day_re = r'((?P\b1(?=er\b)|[1-3]?[02-9](?=e\b)|[1-3]1(?=e\b))(er|e)\b)' + ordinal_day_re = ( + r"((?P\b1(?=er\b)|[1-3]?[02-9](?=e\b)|[1-3]1(?=e\b))(er|e)\b)" + ) - def _ordinal_number(self, n): + def _ordinal_number(self, n: int) -> str: if abs(n) == 1: - return '{0}er'.format(n) - return '{0}e'.format(n) + return f"{n}er" + return f"{n}e" + + +class FrenchLocale(FrenchBaseLocale, Locale): + + names = ["fr", "fr-fr"] + + month_abbreviations = [ + "", + "janv", + "févr", + "mars", + "avr", + "mai", + "juin", + "juil", + "août", + "sept", + "oct", + "nov", + "déc", + ] + + +class FrenchCanadianLocale(FrenchBaseLocale, Locale): + + names = ["fr-ca"] + + month_abbreviations = [ + "", + "janv", + "févr", + "mars", + "avr", + "mai", + "juin", + "juill", + "août", + "sept", + "oct", + "nov", + "déc", + ] class GreekLocale(Locale): - names = ['el', 'el_gr'] + names = ["el", "el-gr"] - past = '{0} πριν' - future = 'σε {0}' + past = "{0} πριν" + future = "σε {0}" + and_word = "και" timeframes = { - 'now': 'τώρα', - 'seconds': 'δευτερόλεπτα', - 'minute': 'ένα λεπτό', - 'minutes': '{0} λεπτά', - 'hour': 'μια ώρα', - 'hours': '{0} ώρες', - 'day': 'μια μέρα', - 'days': '{0} μέρες', - 'month': 'ένα μήνα', - 'months': '{0} μήνες', - 'year': 'ένα χρόνο', - 'years': '{0} χρόνια', + "now": "τώρα", + "second": "ένα δεύτερο", + "seconds": "{0} δευτερόλεπτα", + "minute": "ένα λεπτό", + "minutes": "{0} λεπτά", + "hour": "μία ώρα", + "hours": "{0} ώρες", + "day": "μία μέρα", + "days": "{0} μέρες", + "week": "μία εβδομάδα", + "weeks": "{0} εβδομάδες", + "month": "ένα μήνα", + "months": "{0} μήνες", + "year": "ένα χρόνο", + "years": "{0} χρόνια", } - month_names = ['', 'Ιανουαρίου', 'Φεβρουαρίου', 'Μαρτίου', 'Απριλίου', 'Μαΐου', 'Ιουνίου', - 'Ιουλίου', 'Αυγούστου', 'Σεπτεμβρίου', 'Οκτωβρίου', 'Νοεμβρίου', 'Δεκεμβρίου'] - month_abbreviations = ['', 'Ιαν', 'Φεβ', 'Μαρ', 'Απρ', 'Μαϊ', 'Ιον', 'Ιολ', 'Αυγ', - 'Σεπ', 'Οκτ', 'Νοε', 'Δεκ'] + month_names = [ + "", + "Ιανουαρίου", + "Φεβρουαρίου", + "Μαρτίου", + "Απριλίου", + "Μαΐου", + "Ιουνίου", + "Ιουλίου", + "Αυγούστου", + "Σεπτεμβρίου", + "Οκτωβρίου", + "Νοεμβρίου", + "Δεκεμβρίου", + ] + month_abbreviations = [ + "", + "Ιαν", + "Φεβ", + "Μαρ", + "Απρ", + "Μαϊ", + "Ιον", + "Ιολ", + "Αυγ", + "Σεπ", + "Οκτ", + "Νοε", + "Δεκ", + ] - day_names = ['', 'Δευτέρα', 'Τρίτη', 'Τετάρτη', 'Πέμπτη', 'Παρασκευή', 'Σάββατο', 'Κυριακή'] - day_abbreviations = ['', 'Δευ', 'Τρι', 'Τετ', 'Πεμ', 'Παρ', 'Σαβ', 'Κυρ'] + day_names = [ + "", + "Δευτέρα", + "Τρίτη", + "Τετάρτη", + "Πέμπτη", + "Παρασκευή", + "Σάββατο", + "Κυριακή", + ] + day_abbreviations = ["", "Δευ", "Τρι", "Τετ", "Πεμ", "Παρ", "Σαβ", "Κυρ"] class JapaneseLocale(Locale): - names = ['ja', 'ja_jp'] + names = ["ja", "ja-jp"] - past = '{0}前' - future = '{0}後' + past = "{0}前" + future = "{0}後" + and_word = "" timeframes = { - 'now': '現在', - 'seconds': '数秒', - 'minute': '1分', - 'minutes': '{0}分', - 'hour': '1時間', - 'hours': '{0}時間', - 'day': '1日', - 'days': '{0}日', - 'month': '1ヶ月', - 'months': '{0}ヶ月', - 'year': '1年', - 'years': '{0}年', + "now": "現在", + "second": "1秒", + "seconds": "{0}秒", + "minute": "1分", + "minutes": "{0}分", + "hour": "1時間", + "hours": "{0}時間", + "day": "1日", + "days": "{0}日", + "week": "1週間", + "weeks": "{0}週間", + "month": "1ヶ月", + "months": "{0}ヶ月", + "year": "1年", + "years": "{0}年", } - month_names = ['', '1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', - '9月', '10月', '11月', '12月'] - month_abbreviations = ['', ' 1', ' 2', ' 3', ' 4', ' 5', ' 6', ' 7', ' 8', - ' 9', '10', '11', '12'] + month_names = [ + "", + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月", + ] + month_abbreviations = [ + "", + " 1", + " 2", + " 3", + " 4", + " 5", + " 6", + " 7", + " 8", + " 9", + "10", + "11", + "12", + ] - day_names = ['', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日', '日曜日'] - day_abbreviations = ['', '月', '火', '水', '木', '金', '土', '日'] + day_names = ["", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日", "日曜日"] + day_abbreviations = ["", "月", "火", "水", "木", "金", "土", "日"] class SwedishLocale(Locale): - names = ['sv', 'sv_se'] + names = ["sv", "sv-se"] - past = 'för {0} sen' - future = 'om {0}' + past = "för {0} sen" + future = "om {0}" + and_word = "och" timeframes = { - 'now': 'just nu', - 'seconds': 'några sekunder', - 'minute': 'en minut', - 'minutes': '{0} minuter', - 'hour': 'en timme', - 'hours': '{0} timmar', - 'day': 'en dag', - 'days': '{0} dagar', - 'month': 'en månad', - 'months': '{0} månader', - 'year': 'ett år', - 'years': '{0} år', + "now": "just nu", + "second": "en sekund", + "seconds": "{0} sekunder", + "minute": "en minut", + "minutes": "{0} minuter", + "hour": "en timme", + "hours": "{0} timmar", + "day": "en dag", + "days": "{0} dagar", + "week": "en vecka", + "weeks": "{0} veckor", + "month": "en månad", + "months": "{0} månader", + "year": "ett år", + "years": "{0} år", } - month_names = ['', 'januari', 'februari', 'mars', 'april', 'maj', 'juni', 'juli', - 'augusti', 'september', 'oktober', 'november', 'december'] - month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', - 'aug', 'sep', 'okt', 'nov', 'dec'] + month_names = [ + "", + "januari", + "februari", + "mars", + "april", + "maj", + "juni", + "juli", + "augusti", + "september", + "oktober", + "november", + "december", + ] + month_abbreviations = [ + "", + "jan", + "feb", + "mar", + "apr", + "maj", + "jun", + "jul", + "aug", + "sep", + "okt", + "nov", + "dec", + ] - day_names = ['', 'måndag', 'tisdag', 'onsdag', 'torsdag', 'fredag', 'lördag', 'söndag'] - day_abbreviations = ['', 'mån', 'tis', 'ons', 'tor', 'fre', 'lör', 'sön'] + day_names = [ + "", + "måndag", + "tisdag", + "onsdag", + "torsdag", + "fredag", + "lördag", + "söndag", + ] + day_abbreviations = ["", "mån", "tis", "ons", "tor", "fre", "lör", "sön"] class FinnishLocale(Locale): - names = ['fi', 'fi_fi'] + names = ["fi", "fi-fi"] # The finnish grammar is very complex, and its hard to convert # 1-to-1 to something like English. - past = '{0} sitten' - future = '{0} kuluttua' + past = "{0} sitten" + future = "{0} kuluttua" - timeframes = { - 'now': ['juuri nyt', 'juuri nyt'], - 'seconds': ['muutama sekunti', 'muutaman sekunnin'], - 'minute': ['minuutti', 'minuutin'], - 'minutes': ['{0} minuuttia', '{0} minuutin'], - 'hour': ['tunti', 'tunnin'], - 'hours': ['{0} tuntia', '{0} tunnin'], - 'day': ['päivä', 'päivä'], - 'days': ['{0} päivää', '{0} päivän'], - 'month': ['kuukausi', 'kuukauden'], - 'months': ['{0} kuukautta', '{0} kuukauden'], - 'year': ['vuosi', 'vuoden'], - 'years': ['{0} vuotta', '{0} vuoden'], + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "juuri nyt", + "second": "sekunti", + "seconds": {"past": "{0} muutama sekunti", "future": "{0} muutaman sekunnin"}, + "minute": {"past": "minuutti", "future": "minuutin"}, + "minutes": {"past": "{0} minuuttia", "future": "{0} minuutin"}, + "hour": {"past": "tunti", "future": "tunnin"}, + "hours": {"past": "{0} tuntia", "future": "{0} tunnin"}, + "day": "päivä", + "days": {"past": "{0} päivää", "future": "{0} päivän"}, + "month": {"past": "kuukausi", "future": "kuukauden"}, + "months": {"past": "{0} kuukautta", "future": "{0} kuukauden"}, + "year": {"past": "vuosi", "future": "vuoden"}, + "years": {"past": "{0} vuotta", "future": "{0} vuoden"}, } # Months and days are lowercase in Finnish - month_names = ['', 'tammikuu', 'helmikuu', 'maaliskuu', 'huhtikuu', - 'toukokuu', 'kesäkuu', 'heinäkuu', 'elokuu', - 'syyskuu', 'lokakuu', 'marraskuu', 'joulukuu'] + month_names = [ + "", + "tammikuu", + "helmikuu", + "maaliskuu", + "huhtikuu", + "toukokuu", + "kesäkuu", + "heinäkuu", + "elokuu", + "syyskuu", + "lokakuu", + "marraskuu", + "joulukuu", + ] - month_abbreviations = ['', 'tammi', 'helmi', 'maalis', 'huhti', - 'touko', 'kesä', 'heinä', 'elo', - 'syys', 'loka', 'marras', 'joulu'] + month_abbreviations = [ + "", + "tammi", + "helmi", + "maalis", + "huhti", + "touko", + "kesä", + "heinä", + "elo", + "syys", + "loka", + "marras", + "joulu", + ] - day_names = ['', 'maanantai', 'tiistai', 'keskiviikko', 'torstai', - 'perjantai', 'lauantai', 'sunnuntai'] + day_names = [ + "", + "maanantai", + "tiistai", + "keskiviikko", + "torstai", + "perjantai", + "lauantai", + "sunnuntai", + ] - day_abbreviations = ['', 'ma', 'ti', 'ke', 'to', 'pe', 'la', 'su'] + day_abbreviations = ["", "ma", "ti", "ke", "to", "pe", "la", "su"] - def _format_timeframe(self, timeframe, delta): - return (self.timeframes[timeframe][0].format(abs(delta)), - self.timeframes[timeframe][1].format(abs(delta))) + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + form = self.timeframes[timeframe] - def _format_relative(self, humanized, timeframe, delta): - if timeframe == 'now': - return humanized[0] + if isinstance(form, Mapping): + if delta < 0: + form = form["past"] + else: + form = form["future"] - direction = self.past if delta < 0 else self.future - which = 0 if delta < 0 else 1 + return form.format(abs(delta)) - return direction.format(humanized[which]) - - def _ordinal_number(self, n): - return '{0}.'.format(n) + def _ordinal_number(self, n: int) -> str: + return f"{n}." class ChineseCNLocale(Locale): - names = ['zh', 'zh_cn'] + names = ["zh", "zh-cn"] - past = '{0}前' - future = '{0}后' + past = "{0}前" + future = "{0}后" timeframes = { - 'now': '刚才', - 'seconds': '几秒', - 'minute': '1分钟', - 'minutes': '{0}分钟', - 'hour': '1小时', - 'hours': '{0}小时', - 'day': '1天', - 'days': '{0}天', - 'month': '1个月', - 'months': '{0}个月', - 'year': '1年', - 'years': '{0}年', + "now": "刚才", + "second": "一秒", + "seconds": "{0}秒", + "minute": "1分钟", + "minutes": "{0}分钟", + "hour": "1小时", + "hours": "{0}小时", + "day": "1天", + "days": "{0}天", + "week": "一周", + "weeks": "{0}周", + "month": "1个月", + "months": "{0}个月", + "year": "1年", + "years": "{0}年", } - month_names = ['', '一月', '二月', '三月', '四月', '五月', '六月', '七月', - '八月', '九月', '十月', '十一月', '十二月'] - month_abbreviations = ['', ' 1', ' 2', ' 3', ' 4', ' 5', ' 6', ' 7', ' 8', - ' 9', '10', '11', '12'] + month_names = [ + "", + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月", + ] + month_abbreviations = [ + "", + " 1", + " 2", + " 3", + " 4", + " 5", + " 6", + " 7", + " 8", + " 9", + "10", + "11", + "12", + ] - day_names = ['', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'] - day_abbreviations = ['', '一', '二', '三', '四', '五', '六', '日'] + day_names = ["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] + day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] class ChineseTWLocale(Locale): - names = ['zh_tw'] + names = ["zh-tw"] - past = '{0}前' - future = '{0}後' + past = "{0}前" + future = "{0}後" + and_word = "和" timeframes = { - 'now': '剛才', - 'seconds': '幾秒', - 'minute': '1分鐘', - 'minutes': '{0}分鐘', - 'hour': '1小時', - 'hours': '{0}小時', - 'day': '1天', - 'days': '{0}天', - 'month': '1個月', - 'months': '{0}個月', - 'year': '1年', - 'years': '{0}年', + "now": "剛才", + "second": "1秒", + "seconds": "{0}秒", + "minute": "1分鐘", + "minutes": "{0}分鐘", + "hour": "1小時", + "hours": "{0}小時", + "day": "1天", + "days": "{0}天", + "week": "1週", + "weeks": "{0}週", + "month": "1個月", + "months": "{0}個月", + "year": "1年", + "years": "{0}年", } - month_names = ['', '1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', - '9月', '10月', '11月', '12月'] - month_abbreviations = ['', ' 1', ' 2', ' 3', ' 4', ' 5', ' 6', ' 7', ' 8', - ' 9', '10', '11', '12'] + month_names = [ + "", + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月", + ] + month_abbreviations = [ + "", + " 1", + " 2", + " 3", + " 4", + " 5", + " 6", + " 7", + " 8", + " 9", + "10", + "11", + "12", + ] - day_names = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日'] - day_abbreviations = ['', '一', '二', '三', '四', '五', '六', '日'] + day_names = ["", "週一", "週二", "週三", "週四", "週五", "週六", "週日"] + day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] + + +class HongKongLocale(Locale): + + names = ["zh-hk"] + + past = "{0}前" + future = "{0}後" + + timeframes = { + "now": "剛才", + "second": "1秒", + "seconds": "{0}秒", + "minute": "1分鐘", + "minutes": "{0}分鐘", + "hour": "1小時", + "hours": "{0}小時", + "day": "1天", + "days": "{0}天", + "week": "1星期", + "weeks": "{0}星期", + "month": "1個月", + "months": "{0}個月", + "year": "1年", + "years": "{0}年", + } + + month_names = [ + "", + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月", + ] + month_abbreviations = [ + "", + " 1", + " 2", + " 3", + " 4", + " 5", + " 6", + " 7", + " 8", + " 9", + "10", + "11", + "12", + ] + + day_names = ["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] + day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] class KoreanLocale(Locale): - names = ['ko', 'ko_kr'] + names = ["ko", "ko-kr"] - past = '{0} 전' - future = '{0} 후' + past = "{0} 전" + future = "{0} 후" timeframes = { - 'now': '지금', - 'seconds': '몇 초', - 'minute': '1분', - 'minutes': '{0}분', - 'hour': '1시간', - 'hours': '{0}시간', - 'day': '1일', - 'days': '{0}일', - 'month': '1개월', - 'months': '{0}개월', - 'year': '1년', - 'years': '{0}년', + "now": "지금", + "second": "1초", + "seconds": "{0}초", + "minute": "1분", + "minutes": "{0}분", + "hour": "한시간", + "hours": "{0}시간", + "day": "하루", + "days": "{0}일", + "week": "1주", + "weeks": "{0}주", + "month": "한달", + "months": "{0}개월", + "year": "1년", + "years": "{0}년", } - month_names = ['', '1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', - '9월', '10월', '11월', '12월'] - month_abbreviations = ['', ' 1', ' 2', ' 3', ' 4', ' 5', ' 6', ' 7', ' 8', - ' 9', '10', '11', '12'] + special_dayframes = { + -3: "그끄제", + -2: "그제", + -1: "어제", + 1: "내일", + 2: "모레", + 3: "글피", + 4: "그글피", + } - day_names = ['', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'] - day_abbreviations = ['', '월', '화', '수', '목', '금', '토', '일'] + special_yearframes = {-2: "제작년", -1: "작년", 1: "내년", 2: "내후년"} + + month_names = [ + "", + "1월", + "2월", + "3월", + "4월", + "5월", + "6월", + "7월", + "8월", + "9월", + "10월", + "11월", + "12월", + ] + month_abbreviations = [ + "", + " 1", + " 2", + " 3", + " 4", + " 5", + " 6", + " 7", + " 8", + " 9", + "10", + "11", + "12", + ] + + day_names = ["", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"] + day_abbreviations = ["", "월", "화", "수", "목", "금", "토", "일"] + + def _ordinal_number(self, n: int) -> str: + ordinals = ["0", "첫", "두", "세", "네", "다섯", "여섯", "일곱", "여덟", "아홉", "열"] + if n < len(ordinals): + return f"{ordinals[n]}번째" + return f"{n}번째" + + def _format_relative( + self, + humanized: str, + timeframe: TimeFrameLiteral, + delta: Union[float, int], + ) -> str: + if timeframe in ("day", "days"): + special = self.special_dayframes.get(int(delta)) + if special: + return special + elif timeframe in ("year", "years"): + special = self.special_yearframes.get(int(delta)) + if special: + return special + + return super()._format_relative(humanized, timeframe, delta) # derived locale types & implementations. class DutchLocale(Locale): - names = ['nl', 'nl_nl'] + names = ["nl", "nl-nl"] - past = '{0} geleden' - future = 'over {0}' + past = "{0} geleden" + future = "over {0}" timeframes = { - 'now': 'nu', - 'seconds': 'seconden', - 'minute': 'een minuut', - 'minutes': '{0} minuten', - 'hour': 'een uur', - 'hours': '{0} uur', - 'day': 'een dag', - 'days': '{0} dagen', - 'month': 'een maand', - 'months': '{0} maanden', - 'year': 'een jaar', - 'years': '{0} jaar', + "now": "nu", + "second": "een seconde", + "seconds": "{0} seconden", + "minute": "een minuut", + "minutes": "{0} minuten", + "hour": "een uur", + "hours": "{0} uur", + "day": "een dag", + "days": "{0} dagen", + "week": "een week", + "weeks": "{0} weken", + "month": "een maand", + "months": "{0} maanden", + "year": "een jaar", + "years": "{0} jaar", } # In Dutch names of months and days are not starting with a capital letter # like in the English language. - month_names = ['', 'januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', - 'augustus', 'september', 'oktober', 'november', 'december'] - month_abbreviations = ['', 'jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', - 'sep', 'okt', 'nov', 'dec'] + month_names = [ + "", + "januari", + "februari", + "maart", + "april", + "mei", + "juni", + "juli", + "augustus", + "september", + "oktober", + "november", + "december", + ] + month_abbreviations = [ + "", + "jan", + "feb", + "mrt", + "apr", + "mei", + "jun", + "jul", + "aug", + "sep", + "okt", + "nov", + "dec", + ] - day_names = ['', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag', 'zondag'] - day_abbreviations = ['', 'ma', 'di', 'wo', 'do', 'vr', 'za', 'zo'] + day_names = [ + "", + "maandag", + "dinsdag", + "woensdag", + "donderdag", + "vrijdag", + "zaterdag", + "zondag", + ] + day_abbreviations = ["", "ma", "di", "wo", "do", "vr", "za", "zo"] class SlavicBaseLocale(Locale): + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] - def _format_timeframe(self, timeframe, delta): - + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: form = self.timeframes[timeframe] delta = abs(delta) - if isinstance(form, list): - + if isinstance(form, Mapping): if delta % 10 == 1 and delta % 100 != 11: - form = form[0] + form = form["singular"] elif 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): - form = form[1] + form = form["dual"] else: - form = form[2] + form = form["plural"] return form.format(delta) + class BelarusianLocale(SlavicBaseLocale): - names = ['be', 'be_by'] + names = ["be", "be-by"] - past = '{0} таму' - future = 'праз {0}' + past = "{0} таму" + future = "праз {0}" - timeframes = { - 'now': 'зараз', - 'seconds': 'некалькі секунд', - 'minute': 'хвіліну', - 'minutes': ['{0} хвіліну', '{0} хвіліны', '{0} хвілін'], - 'hour': 'гадзіну', - 'hours': ['{0} гадзіну', '{0} гадзіны', '{0} гадзін'], - 'day': 'дзень', - 'days': ['{0} дзень', '{0} дні', '{0} дзён'], - 'month': 'месяц', - 'months': ['{0} месяц', '{0} месяцы', '{0} месяцаў'], - 'year': 'год', - 'years': ['{0} год', '{0} гады', '{0} гадоў'], + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "зараз", + "second": "секунду", + "seconds": "{0} некалькі секунд", + "minute": "хвіліну", + "minutes": { + "singular": "{0} хвіліну", + "dual": "{0} хвіліны", + "plural": "{0} хвілін", + }, + "hour": "гадзіну", + "hours": { + "singular": "{0} гадзіну", + "dual": "{0} гадзіны", + "plural": "{0} гадзін", + }, + "day": "дзень", + "days": {"singular": "{0} дзень", "dual": "{0} дні", "plural": "{0} дзён"}, + "month": "месяц", + "months": { + "singular": "{0} месяц", + "dual": "{0} месяцы", + "plural": "{0} месяцаў", + }, + "year": "год", + "years": {"singular": "{0} год", "dual": "{0} гады", "plural": "{0} гадоў"}, } - month_names = ['', 'студзеня', 'лютага', 'сакавіка', 'красавіка', 'траўня', 'чэрвеня', - 'ліпеня', 'жніўня', 'верасня', 'кастрычніка', 'лістапада', 'снежня'] - month_abbreviations = ['', 'студ', 'лют', 'сак', 'крас', 'трав', 'чэрв', 'ліп', 'жнів', - 'вер', 'каст', 'ліст', 'снеж'] + month_names = [ + "", + "студзеня", + "лютага", + "сакавіка", + "красавіка", + "траўня", + "чэрвеня", + "ліпеня", + "жніўня", + "верасня", + "кастрычніка", + "лістапада", + "снежня", + ] + month_abbreviations = [ + "", + "студ", + "лют", + "сак", + "крас", + "трав", + "чэрв", + "ліп", + "жнів", + "вер", + "каст", + "ліст", + "снеж", + ] - day_names = ['', 'панядзелак', 'аўторак', 'серада', 'чацвер', 'пятніца', 'субота', 'нядзеля'] - day_abbreviations = ['', 'пн', 'ат', 'ср', 'чц', 'пт', 'сб', 'нд'] + day_names = [ + "", + "панядзелак", + "аўторак", + "серада", + "чацвер", + "пятніца", + "субота", + "нядзеля", + ] + day_abbreviations = ["", "пн", "ат", "ср", "чц", "пт", "сб", "нд"] class PolishLocale(SlavicBaseLocale): - names = ['pl', 'pl_pl'] + names = ["pl", "pl-pl"] - past = '{0} temu' - future = 'za {0}' + past = "{0} temu" + future = "za {0}" - timeframes = { - 'now': 'teraz', - 'seconds': 'kilka sekund', - 'minute': 'minutę', - 'minutes': ['{0} minut', '{0} minuty', '{0} minut'], - 'hour': 'godzina', - 'hours': ['{0} godzin', '{0} godziny', '{0} godzin'], - 'day': 'dzień', - 'days': ['{0} dzień', '{0} dni', '{0} dni'], - 'month': 'miesiąc', - 'months': ['{0} miesiąc', '{0} miesiące', '{0} miesięcy'], - 'year': 'rok', - 'years': ['{0} rok', '{0} lata', '{0} lat'], + # The nouns should be in genitive case (Polish: "dopełniacz") + # in order to correctly form `past` & `future` expressions. + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "teraz", + "second": "sekundę", + "seconds": { + "singular": "{0} sekund", + "dual": "{0} sekundy", + "plural": "{0} sekund", + }, + "minute": "minutę", + "minutes": { + "singular": "{0} minut", + "dual": "{0} minuty", + "plural": "{0} minut", + }, + "hour": "godzinę", + "hours": { + "singular": "{0} godzin", + "dual": "{0} godziny", + "plural": "{0} godzin", + }, + "day": "dzień", + "days": "{0} dni", + "week": "tydzień", + "weeks": { + "singular": "{0} tygodni", + "dual": "{0} tygodnie", + "plural": "{0} tygodni", + }, + "month": "miesiąc", + "months": { + "singular": "{0} miesięcy", + "dual": "{0} miesiące", + "plural": "{0} miesięcy", + }, + "year": "rok", + "years": {"singular": "{0} lat", "dual": "{0} lata", "plural": "{0} lat"}, } - month_names = ['', 'styczeń', 'luty', 'marzec', 'kwiecień', 'maj', - 'czerwiec', 'lipiec', 'sierpień', 'wrzesień', 'październik', - 'listopad', 'grudzień'] - month_abbreviations = ['', 'sty', 'lut', 'mar', 'kwi', 'maj', 'cze', 'lip', - 'sie', 'wrz', 'paź', 'lis', 'gru'] + month_names = [ + "", + "styczeń", + "luty", + "marzec", + "kwiecień", + "maj", + "czerwiec", + "lipiec", + "sierpień", + "wrzesień", + "październik", + "listopad", + "grudzień", + ] + month_abbreviations = [ + "", + "sty", + "lut", + "mar", + "kwi", + "maj", + "cze", + "lip", + "sie", + "wrz", + "paź", + "lis", + "gru", + ] - day_names = ['', 'poniedziałek', 'wtorek', 'środa', 'czwartek', 'piątek', - 'sobota', 'niedziela'] - day_abbreviations = ['', 'Pn', 'Wt', 'Śr', 'Czw', 'Pt', 'So', 'Nd'] + day_names = [ + "", + "poniedziałek", + "wtorek", + "środa", + "czwartek", + "piątek", + "sobota", + "niedziela", + ] + day_abbreviations = ["", "Pn", "Wt", "Śr", "Czw", "Pt", "So", "Nd"] class RussianLocale(SlavicBaseLocale): - names = ['ru', 'ru_ru'] + names = ["ru", "ru-ru"] - past = '{0} назад' - future = 'через {0}' + past = "{0} назад" + future = "через {0}" - timeframes = { - 'now': 'сейчас', - 'seconds': 'несколько секунд', - 'minute': 'минуту', - 'minutes': ['{0} минуту', '{0} минуты', '{0} минут'], - 'hour': 'час', - 'hours': ['{0} час', '{0} часа', '{0} часов'], - 'day': 'день', - 'days': ['{0} день', '{0} дня', '{0} дней'], - 'month': 'месяц', - 'months': ['{0} месяц', '{0} месяца', '{0} месяцев'], - 'year': 'год', - 'years': ['{0} год', '{0} года', '{0} лет'], + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "сейчас", + "second": "Второй", + "seconds": "{0} несколько секунд", + "minute": "минуту", + "minutes": { + "singular": "{0} минуту", + "dual": "{0} минуты", + "plural": "{0} минут", + }, + "hour": "час", + "hours": {"singular": "{0} час", "dual": "{0} часа", "plural": "{0} часов"}, + "day": "день", + "days": {"singular": "{0} день", "dual": "{0} дня", "plural": "{0} дней"}, + "week": "неделю", + "weeks": { + "singular": "{0} неделю", + "dual": "{0} недели", + "plural": "{0} недель", + }, + "month": "месяц", + "months": { + "singular": "{0} месяц", + "dual": "{0} месяца", + "plural": "{0} месяцев", + }, + "year": "год", + "years": {"singular": "{0} год", "dual": "{0} года", "plural": "{0} лет"}, } - month_names = ['', 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', - 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'] - month_abbreviations = ['', 'янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', - 'авг', 'сен', 'окт', 'ноя', 'дек'] + month_names = [ + "", + "января", + "февраля", + "марта", + "апреля", + "мая", + "июня", + "июля", + "августа", + "сентября", + "октября", + "ноября", + "декабря", + ] + month_abbreviations = [ + "", + "янв", + "фев", + "мар", + "апр", + "май", + "июн", + "июл", + "авг", + "сен", + "окт", + "ноя", + "дек", + ] - day_names = ['', 'понедельник', 'вторник', 'среда', 'четверг', 'пятница', - 'суббота', 'воскресенье'] - day_abbreviations = ['', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб', 'вс'] + day_names = [ + "", + "понедельник", + "вторник", + "среда", + "четверг", + "пятница", + "суббота", + "воскресенье", + ] + day_abbreviations = ["", "пн", "вт", "ср", "чт", "пт", "сб", "вс"] + + +class AfrikaansLocale(Locale): + + names = ["af", "af-nl"] + + past = "{0} gelede" + future = "in {0}" + + timeframes = { + "now": "nou", + "second": "n sekonde", + "seconds": "{0} sekondes", + "minute": "minuut", + "minutes": "{0} minute", + "hour": "uur", + "hours": "{0} ure", + "day": "een dag", + "days": "{0} dae", + "month": "een maand", + "months": "{0} maande", + "year": "een jaar", + "years": "{0} jaar", + } + + month_names = [ + "", + "Januarie", + "Februarie", + "Maart", + "April", + "Mei", + "Junie", + "Julie", + "Augustus", + "September", + "Oktober", + "November", + "Desember", + ] + month_abbreviations = [ + "", + "Jan", + "Feb", + "Mrt", + "Apr", + "Mei", + "Jun", + "Jul", + "Aug", + "Sep", + "Okt", + "Nov", + "Des", + ] + + day_names = [ + "", + "Maandag", + "Dinsdag", + "Woensdag", + "Donderdag", + "Vrydag", + "Saterdag", + "Sondag", + ] + day_abbreviations = ["", "Ma", "Di", "Wo", "Do", "Vr", "Za", "So"] class BulgarianLocale(SlavicBaseLocale): - names = ['bg', 'bg_BG'] + names = ["bg", "bg-bg"] - past = '{0} назад' - future = 'напред {0}' + past = "{0} назад" + future = "напред {0}" - timeframes = { - 'now': 'сега', - 'seconds': 'няколко секунди', - 'minute': 'минута', - 'minutes': ['{0} минута', '{0} минути', '{0} минути'], - 'hour': 'час', - 'hours': ['{0} час', '{0} часа', '{0} часа'], - 'day': 'ден', - 'days': ['{0} ден', '{0} дни', '{0} дни'], - 'month': 'месец', - 'months': ['{0} месец', '{0} месеца', '{0} месеца'], - 'year': 'година', - 'years': ['{0} година', '{0} години', '{0} години'], + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "сега", + "second": "секунда", + "seconds": "{0} няколко секунди", + "minute": "минута", + "minutes": { + "singular": "{0} минута", + "dual": "{0} минути", + "plural": "{0} минути", + }, + "hour": "час", + "hours": {"singular": "{0} час", "dual": "{0} часа", "plural": "{0} часа"}, + "day": "ден", + "days": {"singular": "{0} ден", "dual": "{0} дни", "plural": "{0} дни"}, + "month": "месец", + "months": { + "singular": "{0} месец", + "dual": "{0} месеца", + "plural": "{0} месеца", + }, + "year": "година", + "years": { + "singular": "{0} година", + "dual": "{0} години", + "plural": "{0} години", + }, } - month_names = ['', 'януари', 'февруари', 'март', 'април', 'май', 'юни', - 'юли', 'август', 'септември', 'октомври', 'ноември', 'декември'] - month_abbreviations = ['', 'ян', 'февр', 'март', 'апр', 'май', 'юни', 'юли', - 'авг', 'септ', 'окт', 'ноем', 'дек'] + month_names = [ + "", + "януари", + "февруари", + "март", + "април", + "май", + "юни", + "юли", + "август", + "септември", + "октомври", + "ноември", + "декември", + ] + month_abbreviations = [ + "", + "ян", + "февр", + "март", + "апр", + "май", + "юни", + "юли", + "авг", + "септ", + "окт", + "ноем", + "дек", + ] - day_names = ['', 'понеделник', 'вторник', 'сряда', 'четвъртък', 'петък', - 'събота', 'неделя'] - day_abbreviations = ['', 'пон', 'вт', 'ср', 'четв', 'пет', 'съб', 'нед'] + day_names = [ + "", + "понеделник", + "вторник", + "сряда", + "четвъртък", + "петък", + "събота", + "неделя", + ] + day_abbreviations = ["", "пон", "вт", "ср", "четв", "пет", "съб", "нед"] class UkrainianLocale(SlavicBaseLocale): - names = ['ua', 'uk_ua'] + names = ["ua", "uk", "uk-ua"] - past = '{0} тому' - future = 'за {0}' + past = "{0} тому" + future = "за {0}" - timeframes = { - 'now': 'зараз', - 'seconds': 'кілька секунд', - 'minute': 'хвилину', - 'minutes': ['{0} хвилину', '{0} хвилини', '{0} хвилин'], - 'hour': 'годину', - 'hours': ['{0} годину', '{0} години', '{0} годин'], - 'day': 'день', - 'days': ['{0} день', '{0} дні', '{0} днів'], - 'month': 'місяць', - 'months': ['{0} місяць', '{0} місяці', '{0} місяців'], - 'year': 'рік', - 'years': ['{0} рік', '{0} роки', '{0} років'], - } - - month_names = ['', 'січня', 'лютого', 'березня', 'квітня', 'травня', 'червня', - 'липня', 'серпня', 'вересня', 'жовтня', 'листопада', 'грудня'] - month_abbreviations = ['', 'січ', 'лют', 'бер', 'квіт', 'трав', 'черв', 'лип', 'серп', - 'вер', 'жовт', 'лист', 'груд'] - - day_names = ['', 'понеділок', 'вівторок', 'середа', 'четвер', 'п’ятниця', 'субота', 'неділя'] - day_abbreviations = ['', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб', 'нд'] - - -class _DeutschLocaleCommonMixin(object): - - past = 'vor {0}' - future = 'in {0}' - - timeframes = { - 'now': 'gerade eben', - 'seconds': 'Sekunden', - 'minute': 'einer Minute', - 'minutes': '{0} Minuten', - 'hour': 'einer Stunde', - 'hours': '{0} Stunden', - 'day': 'einem Tag', - 'days': '{0} Tagen', - 'month': 'einem Monat', - 'months': '{0} Monaten', - 'year': 'einem Jahr', - 'years': '{0} Jahren', + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "зараз", + "second": "секунда", + "seconds": "{0} кілька секунд", + "minute": "хвилину", + "minutes": { + "singular": "{0} хвилину", + "dual": "{0} хвилини", + "plural": "{0} хвилин", + }, + "hour": "годину", + "hours": { + "singular": "{0} годину", + "dual": "{0} години", + "plural": "{0} годин", + }, + "day": "день", + "days": {"singular": "{0} день", "dual": "{0} дні", "plural": "{0} днів"}, + "month": "місяць", + "months": { + "singular": "{0} місяць", + "dual": "{0} місяці", + "plural": "{0} місяців", + }, + "year": "рік", + "years": {"singular": "{0} рік", "dual": "{0} роки", "plural": "{0} років"}, } month_names = [ - '', 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', - 'August', 'September', 'Oktober', 'November', 'Dezember' + "", + "січня", + "лютого", + "березня", + "квітня", + "травня", + "червня", + "липня", + "серпня", + "вересня", + "жовтня", + "листопада", + "грудня", ] - month_abbreviations = [ - '', 'Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', - 'Okt', 'Nov', 'Dez' + "", + "січ", + "лют", + "бер", + "квіт", + "трав", + "черв", + "лип", + "серп", + "вер", + "жовт", + "лист", + "груд", ] day_names = [ - '', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', - 'Samstag', 'Sonntag' + "", + "понеділок", + "вівторок", + "середа", + "четвер", + "п’ятниця", + "субота", + "неділя", + ] + day_abbreviations = ["", "пн", "вт", "ср", "чт", "пт", "сб", "нд"] + + +class MacedonianLocale(SlavicBaseLocale): + names = ["mk", "mk-mk"] + + past = "пред {0}" + future = "за {0}" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "сега", + "second": "една секунда", + "seconds": { + "singular": "{0} секунда", + "dual": "{0} секунди", + "plural": "{0} секунди", + }, + "minute": "една минута", + "minutes": { + "singular": "{0} минута", + "dual": "{0} минути", + "plural": "{0} минути", + }, + "hour": "еден саат", + "hours": {"singular": "{0} саат", "dual": "{0} саати", "plural": "{0} саати"}, + "day": "еден ден", + "days": {"singular": "{0} ден", "dual": "{0} дена", "plural": "{0} дена"}, + "week": "една недела", + "weeks": { + "singular": "{0} недела", + "dual": "{0} недели", + "plural": "{0} недели", + }, + "month": "еден месец", + "months": { + "singular": "{0} месец", + "dual": "{0} месеци", + "plural": "{0} месеци", + }, + "year": "една година", + "years": { + "singular": "{0} година", + "dual": "{0} години", + "plural": "{0} години", + }, + } + + meridians = {"am": "дп", "pm": "пп", "AM": "претпладне", "PM": "попладне"} + + month_names = [ + "", + "Јануари", + "Февруари", + "Март", + "Април", + "Мај", + "Јуни", + "Јули", + "Август", + "Септември", + "Октомври", + "Ноември", + "Декември", + ] + month_abbreviations = [ + "", + "Јан", + "Фев", + "Мар", + "Апр", + "Мај", + "Јун", + "Јул", + "Авг", + "Септ", + "Окт", + "Ноем", + "Декем", ] + day_names = [ + "", + "Понеделник", + "Вторник", + "Среда", + "Четврток", + "Петок", + "Сабота", + "Недела", + ] day_abbreviations = [ - '', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So' + "", + "Пон", + "Вт", + "Сре", + "Чет", + "Пет", + "Саб", + "Нед", ] - def _ordinal_number(self, n): - return '{0}.'.format(n) + +class GermanBaseLocale(Locale): + + past = "vor {0}" + future = "in {0}" + and_word = "und" + + timeframes = { + "now": "gerade eben", + "second": "einer Sekunde", + "seconds": "{0} Sekunden", + "minute": "einer Minute", + "minutes": "{0} Minuten", + "hour": "einer Stunde", + "hours": "{0} Stunden", + "day": "einem Tag", + "days": "{0} Tagen", + "week": "einer Woche", + "weeks": "{0} Wochen", + "month": "einem Monat", + "months": "{0} Monaten", + "year": "einem Jahr", + "years": "{0} Jahren", + } + + timeframes_only_distance = timeframes.copy() + timeframes_only_distance["second"] = "eine Sekunde" + timeframes_only_distance["minute"] = "eine Minute" + timeframes_only_distance["hour"] = "eine Stunde" + timeframes_only_distance["day"] = "ein Tag" + timeframes_only_distance["days"] = "{0} Tage" + timeframes_only_distance["week"] = "eine Woche" + timeframes_only_distance["month"] = "ein Monat" + timeframes_only_distance["months"] = "{0} Monate" + timeframes_only_distance["year"] = "ein Jahr" + timeframes_only_distance["years"] = "{0} Jahre" + + month_names = [ + "", + "Januar", + "Februar", + "März", + "April", + "Mai", + "Juni", + "Juli", + "August", + "September", + "Oktober", + "November", + "Dezember", + ] + + month_abbreviations = [ + "", + "Jan", + "Feb", + "Mär", + "Apr", + "Mai", + "Jun", + "Jul", + "Aug", + "Sep", + "Okt", + "Nov", + "Dez", + ] + + day_names = [ + "", + "Montag", + "Dienstag", + "Mittwoch", + "Donnerstag", + "Freitag", + "Samstag", + "Sonntag", + ] + + day_abbreviations = ["", "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] + + def _ordinal_number(self, n: int) -> str: + return f"{n}." + + def describe( + self, + timeframe: TimeFrameLiteral, + delta: Union[int, float] = 0, + only_distance: bool = False, + ) -> str: + """Describes a delta within a timeframe in plain language. + + :param timeframe: a string representing a timeframe. + :param delta: a quantity representing a delta in a timeframe. + :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords + """ + + if not only_distance: + return super().describe(timeframe, delta, only_distance) + + # German uses a different case without 'in' or 'ago' + humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) + + return humanized -class GermanLocale(_DeutschLocaleCommonMixin, Locale): +class GermanLocale(GermanBaseLocale, Locale): - names = ['de', 'de_de'] - - timeframes = _DeutschLocaleCommonMixin.timeframes.copy() - timeframes['days'] = '{0} Tagen' + names = ["de", "de-de"] -class AustriaLocale(_DeutschLocaleCommonMixin, Locale): +class SwissLocale(GermanBaseLocale, Locale): - names = ['de', 'de_at'] + names = ["de-ch"] - timeframes = _DeutschLocaleCommonMixin.timeframes.copy() - timeframes['days'] = '{0} Tage' + +class AustrianLocale(GermanBaseLocale, Locale): + + names = ["de-at"] + + month_names = [ + "", + "Jänner", + "Februar", + "März", + "April", + "Mai", + "Juni", + "Juli", + "August", + "September", + "Oktober", + "November", + "Dezember", + ] class NorwegianLocale(Locale): - names = ['nb', 'nb_no'] + names = ["nb", "nb-no"] - past = 'for {0} siden' - future = 'om {0}' + past = "for {0} siden" + future = "om {0}" timeframes = { - 'now': 'nå nettopp', - 'seconds': 'noen sekunder', - 'minute': 'ett minutt', - 'minutes': '{0} minutter', - 'hour': 'en time', - 'hours': '{0} timer', - 'day': 'en dag', - 'days': '{0} dager', - 'month': 'en måned', - 'months': '{0} måneder', - 'year': 'ett år', - 'years': '{0} år', + "now": "nå nettopp", + "second": "ett sekund", + "seconds": "{0} sekunder", + "minute": "ett minutt", + "minutes": "{0} minutter", + "hour": "en time", + "hours": "{0} timer", + "day": "en dag", + "days": "{0} dager", + "month": "en måned", + "months": "{0} måneder", + "year": "ett år", + "years": "{0} år", } - month_names = ['', 'januar', 'februar', 'mars', 'april', 'mai', 'juni', - 'juli', 'august', 'september', 'oktober', 'november', - 'desember'] - month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'mai', 'jun', 'jul', - 'aug', 'sep', 'okt', 'nov', 'des'] + month_names = [ + "", + "januar", + "februar", + "mars", + "april", + "mai", + "juni", + "juli", + "august", + "september", + "oktober", + "november", + "desember", + ] + month_abbreviations = [ + "", + "jan", + "feb", + "mar", + "apr", + "mai", + "jun", + "jul", + "aug", + "sep", + "okt", + "nov", + "des", + ] - day_names = ['', 'mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', - 'lørdag', 'søndag'] - day_abbreviations = ['', 'ma', 'ti', 'on', 'to', 'fr', 'lø', 'sø'] + day_names = [ + "", + "mandag", + "tirsdag", + "onsdag", + "torsdag", + "fredag", + "lørdag", + "søndag", + ] + day_abbreviations = ["", "ma", "ti", "on", "to", "fr", "lø", "sø"] class NewNorwegianLocale(Locale): - names = ['nn', 'nn_no'] + names = ["nn", "nn-no"] - past = 'for {0} sidan' - future = 'om {0}' + past = "for {0} sidan" + future = "om {0}" timeframes = { - 'now': 'no nettopp', - 'seconds': 'nokre sekund', - 'minute': 'ett minutt', - 'minutes': '{0} minutt', - 'hour': 'ein time', - 'hours': '{0} timar', - 'day': 'ein dag', - 'days': '{0} dagar', - 'month': 'en månad', - 'months': '{0} månader', - 'year': 'eit år', - 'years': '{0} år', + "now": "no nettopp", + "second": "eitt sekund", + "seconds": "{0} sekund", + "minute": "eitt minutt", + "minutes": "{0} minutt", + "hour": "ein time", + "hours": "{0} timar", + "day": "ein dag", + "days": "{0} dagar", + "month": "en månad", + "months": "{0} månader", + "year": "eitt år", + "years": "{0} år", } - month_names = ['', 'januar', 'februar', 'mars', 'april', 'mai', 'juni', - 'juli', 'august', 'september', 'oktober', 'november', - 'desember'] - month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'mai', 'jun', 'jul', - 'aug', 'sep', 'okt', 'nov', 'des'] + month_names = [ + "", + "januar", + "februar", + "mars", + "april", + "mai", + "juni", + "juli", + "august", + "september", + "oktober", + "november", + "desember", + ] + month_abbreviations = [ + "", + "jan", + "feb", + "mar", + "apr", + "mai", + "jun", + "jul", + "aug", + "sep", + "okt", + "nov", + "des", + ] - day_names = ['', 'måndag', 'tysdag', 'onsdag', 'torsdag', 'fredag', - 'laurdag', 'sundag'] - day_abbreviations = ['', 'må', 'ty', 'on', 'to', 'fr', 'la', 'su'] + day_names = [ + "", + "måndag", + "tysdag", + "onsdag", + "torsdag", + "fredag", + "laurdag", + "sundag", + ] + day_abbreviations = ["", "må", "ty", "on", "to", "fr", "la", "su"] class PortugueseLocale(Locale): - names = ['pt', 'pt_pt'] + names = ["pt", "pt-pt"] - past = 'há {0}' - future = 'em {0}' + past = "há {0}" + future = "em {0}" + and_word = "e" timeframes = { - 'now': 'agora', - 'seconds': 'segundos', - 'minute': 'um minuto', - 'minutes': '{0} minutos', - 'hour': 'uma hora', - 'hours': '{0} horas', - 'day': 'um dia', - 'days': '{0} dias', - 'month': 'um mês', - 'months': '{0} meses', - 'year': 'um ano', - 'years': '{0} anos', + "now": "agora", + "second": "um segundo", + "seconds": "{0} segundos", + "minute": "um minuto", + "minutes": "{0} minutos", + "hour": "uma hora", + "hours": "{0} horas", + "day": "um dia", + "days": "{0} dias", + "week": "uma semana", + "weeks": "{0} semanas", + "month": "um mês", + "months": "{0} meses", + "year": "um ano", + "years": "{0} anos", } - month_names = ['', 'janeiro', 'fevereiro', 'março', 'abril', 'maio', 'junho', 'julho', - 'agosto', 'setembro', 'outubro', 'novembro', 'dezembro'] - month_abbreviations = ['', 'jan', 'fev', 'mar', 'abr', 'maio', 'jun', 'jul', 'ago', - 'set', 'out', 'nov', 'dez'] + month_names = [ + "", + "Janeiro", + "Fevereiro", + "Março", + "Abril", + "Maio", + "Junho", + "Julho", + "Agosto", + "Setembro", + "Outubro", + "Novembro", + "Dezembro", + ] + month_abbreviations = [ + "", + "Jan", + "Fev", + "Mar", + "Abr", + "Mai", + "Jun", + "Jul", + "Ago", + "Set", + "Out", + "Nov", + "Dez", + ] - day_names = ['', 'segunda-feira', 'terça-feira', 'quarta-feira', 'quinta-feira', 'sexta-feira', - 'sábado', 'domingo'] - day_abbreviations = ['', 'seg', 'ter', 'qua', 'qui', 'sex', 'sab', 'dom'] + day_names = [ + "", + "Segunda-feira", + "Terça-feira", + "Quarta-feira", + "Quinta-feira", + "Sexta-feira", + "Sábado", + "Domingo", + ] + day_abbreviations = ["", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab", "Dom"] class BrazilianPortugueseLocale(PortugueseLocale): - names = ['pt_br'] + names = ["pt-br"] - past = 'fazem {0}' + past = "faz {0}" class TagalogLocale(Locale): - names = ['tl'] + names = ["tl", "tl-ph"] - past = 'nakaraang {0}' - future = '{0} mula ngayon' + past = "nakaraang {0}" + future = "{0} mula ngayon" timeframes = { - 'now': 'ngayon lang', - 'seconds': 'segundo', - 'minute': 'isang minuto', - 'minutes': '{0} minuto', - 'hour': 'isang oras', - 'hours': '{0} oras', - 'day': 'isang araw', - 'days': '{0} araw', - 'month': 'isang buwan', - 'months': '{0} buwan', - 'year': 'isang taon', - 'years': '{0} taon', + "now": "ngayon lang", + "second": "isang segundo", + "seconds": "{0} segundo", + "minute": "isang minuto", + "minutes": "{0} minuto", + "hour": "isang oras", + "hours": "{0} oras", + "day": "isang araw", + "days": "{0} araw", + "week": "isang linggo", + "weeks": "{0} linggo", + "month": "isang buwan", + "months": "{0} buwan", + "year": "isang taon", + "years": "{0} taon", } - month_names = ['', 'Enero', 'Pebrero', 'Marso', 'Abril', 'Mayo', 'Hunyo', 'Hulyo', - 'Agosto', 'Setyembre', 'Oktubre', 'Nobyembre', 'Disyembre'] - month_abbreviations = ['', 'Ene', 'Peb', 'Mar', 'Abr', 'May', 'Hun', 'Hul', 'Ago', - 'Set', 'Okt', 'Nob', 'Dis'] + month_names = [ + "", + "Enero", + "Pebrero", + "Marso", + "Abril", + "Mayo", + "Hunyo", + "Hulyo", + "Agosto", + "Setyembre", + "Oktubre", + "Nobyembre", + "Disyembre", + ] + month_abbreviations = [ + "", + "Ene", + "Peb", + "Mar", + "Abr", + "May", + "Hun", + "Hul", + "Ago", + "Set", + "Okt", + "Nob", + "Dis", + ] - day_names = ['', 'Lunes', 'Martes', 'Miyerkules', 'Huwebes', 'Biyernes', 'Sabado', 'Linggo'] - day_abbreviations = ['', 'Lun', 'Mar', 'Miy', 'Huw', 'Biy', 'Sab', 'Lin'] + day_names = [ + "", + "Lunes", + "Martes", + "Miyerkules", + "Huwebes", + "Biyernes", + "Sabado", + "Linggo", + ] + day_abbreviations = ["", "Lun", "Mar", "Miy", "Huw", "Biy", "Sab", "Lin"] + + meridians = {"am": "nu", "pm": "nh", "AM": "ng umaga", "PM": "ng hapon"} + + def _ordinal_number(self, n: int) -> str: + return f"ika-{n}" class VietnameseLocale(Locale): - names = ['vi', 'vi_vn'] + names = ["vi", "vi-vn"] - past = '{0} trước' - future = '{0} nữa' + past = "{0} trước" + future = "{0} nữa" timeframes = { - 'now': 'hiện tại', - 'seconds': 'giây', - 'minute': 'một phút', - 'minutes': '{0} phút', - 'hour': 'một giờ', - 'hours': '{0} giờ', - 'day': 'một ngày', - 'days': '{0} ngày', - 'month': 'một tháng', - 'months': '{0} tháng', - 'year': 'một năm', - 'years': '{0} năm', + "now": "hiện tại", + "second": "một giây", + "seconds": "{0} giây", + "minute": "một phút", + "minutes": "{0} phút", + "hour": "một giờ", + "hours": "{0} giờ", + "day": "một ngày", + "days": "{0} ngày", + "week": "một tuần", + "weeks": "{0} tuần", + "month": "một tháng", + "months": "{0} tháng", + "year": "một năm", + "years": "{0} năm", } - month_names = ['', 'Tháng Một', 'Tháng Hai', 'Tháng Ba', 'Tháng Tư', 'Tháng Năm', 'Tháng Sáu', 'Tháng Bảy', - 'Tháng Tám', 'Tháng Chín', 'Tháng Mười', 'Tháng Mười Một', 'Tháng Mười Hai'] - month_abbreviations = ['', 'Tháng 1', 'Tháng 2', 'Tháng 3', 'Tháng 4', 'Tháng 5', 'Tháng 6', 'Tháng 7', 'Tháng 8', - 'Tháng 9', 'Tháng 10', 'Tháng 11', 'Tháng 12'] + month_names = [ + "", + "Tháng Một", + "Tháng Hai", + "Tháng Ba", + "Tháng Tư", + "Tháng Năm", + "Tháng Sáu", + "Tháng Bảy", + "Tháng Tám", + "Tháng Chín", + "Tháng Mười", + "Tháng Mười Một", + "Tháng Mười Hai", + ] + month_abbreviations = [ + "", + "Tháng 1", + "Tháng 2", + "Tháng 3", + "Tháng 4", + "Tháng 5", + "Tháng 6", + "Tháng 7", + "Tháng 8", + "Tháng 9", + "Tháng 10", + "Tháng 11", + "Tháng 12", + ] - day_names = ['', 'Thứ Hai', 'Thứ Ba', 'Thứ Tư', 'Thứ Năm', 'Thứ Sáu', 'Thứ Bảy', 'Chủ Nhật'] - day_abbreviations = ['', 'Thứ 2', 'Thứ 3', 'Thứ 4', 'Thứ 5', 'Thứ 6', 'Thứ 7', 'CN'] + day_names = [ + "", + "Thứ Hai", + "Thứ Ba", + "Thứ Tư", + "Thứ Năm", + "Thứ Sáu", + "Thứ Bảy", + "Chủ Nhật", + ] + day_abbreviations = ["", "Thứ 2", "Thứ 3", "Thứ 4", "Thứ 5", "Thứ 6", "Thứ 7", "CN"] class TurkishLocale(Locale): - names = ['tr', 'tr_tr'] + names = ["tr", "tr-tr"] - past = '{0} önce' - future = '{0} sonra' + past = "{0} önce" + future = "{0} sonra" + and_word = "ve" timeframes = { - 'now': 'şimdi', - 'seconds': 'saniye', - 'minute': 'bir dakika', - 'minutes': '{0} dakika', - 'hour': 'bir saat', - 'hours': '{0} saat', - 'day': 'bir gün', - 'days': '{0} gün', - 'month': 'bir ay', - 'months': '{0} ay', - 'year': 'yıl', - 'years': '{0} yıl', + "now": "şimdi", + "second": "bir saniye", + "seconds": "{0} saniye", + "minute": "bir dakika", + "minutes": "{0} dakika", + "hour": "bir saat", + "hours": "{0} saat", + "day": "bir gün", + "days": "{0} gün", + "week": "bir hafta", + "weeks": "{0} hafta", + "month": "bir ay", + "months": "{0} ay", + "year": "bir yıl", + "years": "{0} yıl", } - month_names = ['', 'Ocak', 'Şubat', 'Mart', 'Nisan', 'Mayıs', 'Haziran', 'Temmuz', - 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık'] - month_abbreviations = ['', 'Oca', 'Şub', 'Mar', 'Nis', 'May', 'Haz', 'Tem', 'Ağu', - 'Eyl', 'Eki', 'Kas', 'Ara'] + meridians = {"am": "öö", "pm": "ös", "AM": "ÖÖ", "PM": "ÖS"} - day_names = ['', 'Pazartesi', 'Salı', 'Çarşamba', 'Perşembe', 'Cuma', 'Cumartesi', 'Pazar'] - day_abbreviations = ['', 'Pzt', 'Sal', 'Çar', 'Per', 'Cum', 'Cmt', 'Paz'] + month_names = [ + "", + "Ocak", + "Şubat", + "Mart", + "Nisan", + "Mayıs", + "Haziran", + "Temmuz", + "Ağustos", + "Eylül", + "Ekim", + "Kasım", + "Aralık", + ] + month_abbreviations = [ + "", + "Oca", + "Şub", + "Mar", + "Nis", + "May", + "Haz", + "Tem", + "Ağu", + "Eyl", + "Eki", + "Kas", + "Ara", + ] + + day_names = [ + "", + "Pazartesi", + "Salı", + "Çarşamba", + "Perşembe", + "Cuma", + "Cumartesi", + "Pazar", + ] + day_abbreviations = ["", "Pzt", "Sal", "Çar", "Per", "Cum", "Cmt", "Paz"] class AzerbaijaniLocale(Locale): - names = ['az', 'az_az'] + names = ["az", "az-az"] - past = '{0} əvvəl' - future = '{0} sonra' + past = "{0} əvvəl" + future = "{0} sonra" timeframes = { - 'now': 'indi', - 'seconds': 'saniyə', - 'minute': 'bir dəqiqə', - 'minutes': '{0} dəqiqə', - 'hour': 'bir saat', - 'hours': '{0} saat', - 'day': 'bir gün', - 'days': '{0} gün', - 'month': 'bir ay', - 'months': '{0} ay', - 'year': 'il', - 'years': '{0} il', + "now": "indi", + "second": "saniyə", + "seconds": "{0} saniyə", + "minute": "bir dəqiqə", + "minutes": "{0} dəqiqə", + "hour": "bir saat", + "hours": "{0} saat", + "day": "bir gün", + "days": "{0} gün", + "month": "bir ay", + "months": "{0} ay", + "year": "il", + "years": "{0} il", } - month_names = ['', 'Yanvar', 'Fevral', 'Mart', 'Aprel', 'May', 'İyun', 'İyul', - 'Avqust', 'Sentyabr', 'Oktyabr', 'Noyabr', 'Dekabr'] - month_abbreviations = ['', 'Yan', 'Fev', 'Mar', 'Apr', 'May', 'İyn', 'İyl', 'Avq', - 'Sen', 'Okt', 'Noy', 'Dek'] + month_names = [ + "", + "Yanvar", + "Fevral", + "Mart", + "Aprel", + "May", + "İyun", + "İyul", + "Avqust", + "Sentyabr", + "Oktyabr", + "Noyabr", + "Dekabr", + ] + month_abbreviations = [ + "", + "Yan", + "Fev", + "Mar", + "Apr", + "May", + "İyn", + "İyl", + "Avq", + "Sen", + "Okt", + "Noy", + "Dek", + ] - day_names = ['', 'Bazar ertəsi', 'Çərşənbə axşamı', 'Çərşənbə', 'Cümə axşamı', 'Cümə', 'Şənbə', 'Bazar'] - day_abbreviations = ['', 'Ber', 'Çax', 'Çər', 'Cax', 'Cüm', 'Şnb', 'Bzr'] + day_names = [ + "", + "Bazar ertəsi", + "Çərşənbə axşamı", + "Çərşənbə", + "Cümə axşamı", + "Cümə", + "Şənbə", + "Bazar", + ] + day_abbreviations = ["", "Ber", "Çax", "Çər", "Cax", "Cüm", "Şnb", "Bzr"] class ArabicLocale(Locale): + names = [ + "ar", + "ar-ae", + "ar-bh", + "ar-dj", + "ar-eg", + "ar-eh", + "ar-er", + "ar-km", + "ar-kw", + "ar-ly", + "ar-om", + "ar-qa", + "ar-sa", + "ar-sd", + "ar-so", + "ar-ss", + "ar-td", + "ar-ye", + ] - names = ['ar', 'ar_eg'] + past = "منذ {0}" + future = "خلال {0}" - past = 'منذ {0}' - future = 'خلال {0}' - - timeframes = { - 'now': 'الآن', - 'seconds': 'ثوان', - 'minute': 'دقيقة', - 'minutes': '{0} دقائق', - 'hour': 'ساعة', - 'hours': '{0} ساعات', - 'day': 'يوم', - 'days': '{0} أيام', - 'month': 'شهر', - 'months': '{0} شهور', - 'year': 'سنة', - 'years': '{0} سنوات', + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "الآن", + "second": "ثانية", + "seconds": {"2": "ثانيتين", "ten": "{0} ثوان", "higher": "{0} ثانية"}, + "minute": "دقيقة", + "minutes": {"2": "دقيقتين", "ten": "{0} دقائق", "higher": "{0} دقيقة"}, + "hour": "ساعة", + "hours": {"2": "ساعتين", "ten": "{0} ساعات", "higher": "{0} ساعة"}, + "day": "يوم", + "days": {"2": "يومين", "ten": "{0} أيام", "higher": "{0} يوم"}, + "month": "شهر", + "months": {"2": "شهرين", "ten": "{0} أشهر", "higher": "{0} شهر"}, + "year": "سنة", + "years": {"2": "سنتين", "ten": "{0} سنوات", "higher": "{0} سنة"}, } - month_names = ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', - 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] - month_abbreviations = ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', - 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] + month_names = [ + "", + "يناير", + "فبراير", + "مارس", + "أبريل", + "مايو", + "يونيو", + "يوليو", + "أغسطس", + "سبتمبر", + "أكتوبر", + "نوفمبر", + "ديسمبر", + ] + month_abbreviations = [ + "", + "يناير", + "فبراير", + "مارس", + "أبريل", + "مايو", + "يونيو", + "يوليو", + "أغسطس", + "سبتمبر", + "أكتوبر", + "نوفمبر", + "ديسمبر", + ] - day_names = ['', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد'] - day_abbreviations = ['', 'اثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت', 'أحد'] + day_names = [ + "", + "الإثنين", + "الثلاثاء", + "الأربعاء", + "الخميس", + "الجمعة", + "السبت", + "الأحد", + ] + day_abbreviations = ["", "إثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت", "أحد"] + + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + form = self.timeframes[timeframe] + delta = abs(delta) + if isinstance(form, Mapping): + if delta == 2: + form = form["2"] + elif 2 < delta <= 10: + form = form["ten"] + else: + form = form["higher"] + + return form.format(delta) + + +class LevantArabicLocale(ArabicLocale): + names = ["ar-iq", "ar-jo", "ar-lb", "ar-ps", "ar-sy"] + month_names = [ + "", + "كانون الثاني", + "شباط", + "آذار", + "نيسان", + "أيار", + "حزيران", + "تموز", + "آب", + "أيلول", + "تشرين الأول", + "تشرين الثاني", + "كانون الأول", + ] + month_abbreviations = [ + "", + "كانون الثاني", + "شباط", + "آذار", + "نيسان", + "أيار", + "حزيران", + "تموز", + "آب", + "أيلول", + "تشرين الأول", + "تشرين الثاني", + "كانون الأول", + ] + + +class AlgeriaTunisiaArabicLocale(ArabicLocale): + names = ["ar-tn", "ar-dz"] + month_names = [ + "", + "جانفي", + "فيفري", + "مارس", + "أفريل", + "ماي", + "جوان", + "جويلية", + "أوت", + "سبتمبر", + "أكتوبر", + "نوفمبر", + "ديسمبر", + ] + month_abbreviations = [ + "", + "جانفي", + "فيفري", + "مارس", + "أفريل", + "ماي", + "جوان", + "جويلية", + "أوت", + "سبتمبر", + "أكتوبر", + "نوفمبر", + "ديسمبر", + ] + + +class MauritaniaArabicLocale(ArabicLocale): + names = ["ar-mr"] + month_names = [ + "", + "يناير", + "فبراير", + "مارس", + "إبريل", + "مايو", + "يونيو", + "يوليو", + "أغشت", + "شتمبر", + "أكتوبر", + "نوفمبر", + "دجمبر", + ] + month_abbreviations = [ + "", + "يناير", + "فبراير", + "مارس", + "إبريل", + "مايو", + "يونيو", + "يوليو", + "أغشت", + "شتمبر", + "أكتوبر", + "نوفمبر", + "دجمبر", + ] + + +class MoroccoArabicLocale(ArabicLocale): + names = ["ar-ma"] + month_names = [ + "", + "يناير", + "فبراير", + "مارس", + "أبريل", + "ماي", + "يونيو", + "يوليوز", + "غشت", + "شتنبر", + "أكتوبر", + "نونبر", + "دجنبر", + ] + month_abbreviations = [ + "", + "يناير", + "فبراير", + "مارس", + "أبريل", + "ماي", + "يونيو", + "يوليوز", + "غشت", + "شتنبر", + "أكتوبر", + "نونبر", + "دجنبر", + ] class IcelandicLocale(Locale): + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + form = self.timeframes[timeframe] - def _format_timeframe(self, timeframe, delta): + if isinstance(form, Mapping): + if delta < 0: + form = form["past"] + elif delta > 0: + form = form["future"] + else: + raise ValueError( + "Icelandic Locale does not support units with a delta of zero. " + "Please consider making a contribution to fix this issue." + ) + # FIXME: handle when delta is 0 - timeframe = self.timeframes[timeframe] - if delta < 0: - timeframe = timeframe[0] - elif delta > 0: - timeframe = timeframe[1] + return form.format(abs(delta)) - return timeframe.format(abs(delta)) + names = ["is", "is-is"] - names = ['is', 'is_is'] + past = "fyrir {0} síðan" + future = "eftir {0}" - past = 'fyrir {0} síðan' - future = 'eftir {0}' - - timeframes = { - 'now': 'rétt í þessu', - 'seconds': ('nokkrum sekúndum', 'nokkrar sekúndur'), - 'minute': ('einni mínútu', 'eina mínútu'), - 'minutes': ('{0} mínútum', '{0} mínútur'), - 'hour': ('einum tíma', 'einn tíma'), - 'hours': ('{0} tímum', '{0} tíma'), - 'day': ('einum degi', 'einn dag'), - 'days': ('{0} dögum', '{0} daga'), - 'month': ('einum mánuði', 'einn mánuð'), - 'months': ('{0} mánuðum', '{0} mánuði'), - 'year': ('einu ári', 'eitt ár'), - 'years': ('{0} árum', '{0} ár'), + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "rétt í þessu", + "second": {"past": "sekúndu", "future": "sekúndu"}, + "seconds": {"past": "{0} nokkrum sekúndum", "future": "nokkrar sekúndur"}, + "minute": {"past": "einni mínútu", "future": "eina mínútu"}, + "minutes": {"past": "{0} mínútum", "future": "{0} mínútur"}, + "hour": {"past": "einum tíma", "future": "einn tíma"}, + "hours": {"past": "{0} tímum", "future": "{0} tíma"}, + "day": {"past": "einum degi", "future": "einn dag"}, + "days": {"past": "{0} dögum", "future": "{0} daga"}, + "month": {"past": "einum mánuði", "future": "einn mánuð"}, + "months": {"past": "{0} mánuðum", "future": "{0} mánuði"}, + "year": {"past": "einu ári", "future": "eitt ár"}, + "years": {"past": "{0} árum", "future": "{0} ár"}, } - meridians = { - 'am': 'f.h.', - 'pm': 'e.h.', - 'AM': 'f.h.', - 'PM': 'e.h.', - } + meridians = {"am": "f.h.", "pm": "e.h.", "AM": "f.h.", "PM": "e.h."} - month_names = ['', 'janúar', 'febrúar', 'mars', 'apríl', 'maí', 'júní', - 'júlí', 'ágúst', 'september', 'október', 'nóvember', 'desember'] - month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'maí', 'jún', - 'júl', 'ágú', 'sep', 'okt', 'nóv', 'des'] + month_names = [ + "", + "janúar", + "febrúar", + "mars", + "apríl", + "maí", + "júní", + "júlí", + "ágúst", + "september", + "október", + "nóvember", + "desember", + ] + month_abbreviations = [ + "", + "jan", + "feb", + "mar", + "apr", + "maí", + "jún", + "júl", + "ágú", + "sep", + "okt", + "nóv", + "des", + ] - day_names = ['', 'mánudagur', 'þriðjudagur', 'miðvikudagur', 'fimmtudagur', - 'föstudagur', 'laugardagur', 'sunnudagur'] - day_abbreviations = ['', 'mán', 'þri', 'mið', 'fim', 'fös', 'lau', 'sun'] + day_names = [ + "", + "mánudagur", + "þriðjudagur", + "miðvikudagur", + "fimmtudagur", + "föstudagur", + "laugardagur", + "sunnudagur", + ] + day_abbreviations = ["", "mán", "þri", "mið", "fim", "fös", "lau", "sun"] class DanishLocale(Locale): - names = ['da', 'da_dk'] + names = ["da", "da-dk"] - past = 'for {0} siden' - future = 'efter {0}' + past = "for {0} siden" + future = "efter {0}" + and_word = "og" timeframes = { - 'now': 'lige nu', - 'seconds': 'et par sekunder', - 'minute': 'et minut', - 'minutes': '{0} minutter', - 'hour': 'en time', - 'hours': '{0} timer', - 'day': 'en dag', - 'days': '{0} dage', - 'month': 'en måned', - 'months': '{0} måneder', - 'year': 'et år', - 'years': '{0} år', + "now": "lige nu", + "second": "et sekund", + "seconds": "{0} et par sekunder", + "minute": "et minut", + "minutes": "{0} minutter", + "hour": "en time", + "hours": "{0} timer", + "day": "en dag", + "days": "{0} dage", + "month": "en måned", + "months": "{0} måneder", + "year": "et år", + "years": "{0} år", } - month_names = ['', 'januar', 'februar', 'marts', 'april', 'maj', 'juni', - 'juli', 'august', 'september', 'oktober', 'november', 'december'] - month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'maj', 'jun', - 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'] + month_names = [ + "", + "januar", + "februar", + "marts", + "april", + "maj", + "juni", + "juli", + "august", + "september", + "oktober", + "november", + "december", + ] + month_abbreviations = [ + "", + "jan", + "feb", + "mar", + "apr", + "maj", + "jun", + "jul", + "aug", + "sep", + "okt", + "nov", + "dec", + ] - day_names = ['', 'mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', - 'lørdag', 'søndag'] - day_abbreviations = ['', 'man', 'tir', 'ons', 'tor', 'fre', 'lør', 'søn'] + day_names = [ + "", + "mandag", + "tirsdag", + "onsdag", + "torsdag", + "fredag", + "lørdag", + "søndag", + ] + day_abbreviations = ["", "man", "tir", "ons", "tor", "fre", "lør", "søn"] class MalayalamLocale(Locale): - names = ['ml'] + names = ["ml"] - past = '{0} മുമ്പ്' - future = '{0} ശേഷം' + past = "{0} മുമ്പ്" + future = "{0} ശേഷം" timeframes = { - 'now': 'ഇപ്പോൾ', - 'seconds': 'സെക്കന്റ്‌', - 'minute': 'ഒരു മിനിറ്റ്', - 'minutes': '{0} മിനിറ്റ്', - 'hour': 'ഒരു മണിക്കൂർ', - 'hours': '{0} മണിക്കൂർ', - 'day': 'ഒരു ദിവസം ', - 'days': '{0} ദിവസം ', - 'month': 'ഒരു മാസം ', - 'months': '{0} മാസം ', - 'year': 'ഒരു വർഷം ', - 'years': '{0} വർഷം ', + "now": "ഇപ്പോൾ", + "second": "ഒരു നിമിഷം", + "seconds": "{0} സെക്കന്റ്‌", + "minute": "ഒരു മിനിറ്റ്", + "minutes": "{0} മിനിറ്റ്", + "hour": "ഒരു മണിക്കൂർ", + "hours": "{0} മണിക്കൂർ", + "day": "ഒരു ദിവസം ", + "days": "{0} ദിവസം ", + "month": "ഒരു മാസം ", + "months": "{0} മാസം ", + "year": "ഒരു വർഷം ", + "years": "{0} വർഷം ", } meridians = { - 'am': 'രാവിലെ', - 'pm': 'ഉച്ചക്ക് ശേഷം', - 'AM': 'രാവിലെ', - 'PM': 'ഉച്ചക്ക് ശേഷം', + "am": "രാവിലെ", + "pm": "ഉച്ചക്ക് ശേഷം", + "AM": "രാവിലെ", + "PM": "ഉച്ചക്ക് ശേഷം", } - month_names = ['', 'ജനുവരി', 'ഫെബ്രുവരി', 'മാർച്ച്‌', 'ഏപ്രിൽ ', 'മെയ്‌ ', 'ജൂണ്‍', 'ജൂലൈ', - 'ഓഗസ്റ്റ്‌', 'സെപ്റ്റംബർ', 'ഒക്ടോബർ', 'നവംബർ', 'ഡിസംബർ'] - month_abbreviations = ['', 'ജനു', 'ഫെബ് ', 'മാർ', 'ഏപ്രിൽ', 'മേയ്', 'ജൂണ്‍', 'ജൂലൈ', 'ഓഗസ്റ', - 'സെപ്റ്റ', 'ഒക്ടോ', 'നവം', 'ഡിസം'] + month_names = [ + "", + "ജനുവരി", + "ഫെബ്രുവരി", + "മാർച്ച്‌", + "ഏപ്രിൽ ", + "മെയ്‌ ", + "ജൂണ്‍", + "ജൂലൈ", + "ഓഗസ്റ്റ്‌", + "സെപ്റ്റംബർ", + "ഒക്ടോബർ", + "നവംബർ", + "ഡിസംബർ", + ] + month_abbreviations = [ + "", + "ജനു", + "ഫെബ് ", + "മാർ", + "ഏപ്രിൽ", + "മേയ്", + "ജൂണ്‍", + "ജൂലൈ", + "ഓഗസ്റ", + "സെപ്റ്റ", + "ഒക്ടോ", + "നവം", + "ഡിസം", + ] - day_names = ['', 'തിങ്കള്‍', 'ചൊവ്വ', 'ബുധന്‍', 'വ്യാഴം', 'വെള്ളി', 'ശനി', 'ഞായര്‍'] - day_abbreviations = ['', 'തിങ്കള്‍', 'ചൊവ്വ', 'ബുധന്‍', 'വ്യാഴം', 'വെള്ളി', 'ശനി', 'ഞായര്‍'] + day_names = ["", "തിങ്കള്‍", "ചൊവ്വ", "ബുധന്‍", "വ്യാഴം", "വെള്ളി", "ശനി", "ഞായര്‍"] + day_abbreviations = [ + "", + "തിങ്കള്‍", + "ചൊവ്വ", + "ബുധന്‍", + "വ്യാഴം", + "വെള്ളി", + "ശനി", + "ഞായര്‍", + ] class HindiLocale(Locale): - names = ['hi'] + names = ["hi", "hi-in"] - past = '{0} पहले' - future = '{0} बाद' + past = "{0} पहले" + future = "{0} बाद" timeframes = { - 'now': 'अभी', - 'seconds': 'सेकंड्', - 'minute': 'एक मिनट ', - 'minutes': '{0} मिनट ', - 'hour': 'एक घंटा', - 'hours': '{0} घंटे', - 'day': 'एक दिन', - 'days': '{0} दिन', - 'month': 'एक माह ', - 'months': '{0} महीने ', - 'year': 'एक वर्ष ', - 'years': '{0} साल ', + "now": "अभी", + "second": "एक पल", + "seconds": "{0} सेकंड्", + "minute": "एक मिनट ", + "minutes": "{0} मिनट ", + "hour": "एक घंटा", + "hours": "{0} घंटे", + "day": "एक दिन", + "days": "{0} दिन", + "month": "एक माह ", + "months": "{0} महीने ", + "year": "एक वर्ष ", + "years": "{0} साल ", } - meridians = { - 'am': 'सुबह', - 'pm': 'शाम', - 'AM': 'सुबह', - 'PM': 'शाम', - } + meridians = {"am": "सुबह", "pm": "शाम", "AM": "सुबह", "PM": "शाम"} - month_names = ['', 'जनवरी', 'फरवरी', 'मार्च', 'अप्रैल ', 'मई', 'जून', 'जुलाई', - 'अगस्त', 'सितंबर', 'अक्टूबर', 'नवंबर', 'दिसंबर'] - month_abbreviations = ['', 'जन', 'फ़र', 'मार्च', 'अप्रै', 'मई', 'जून', 'जुलाई', 'आग', - 'सित', 'अकत', 'नवे', 'दिस'] + month_names = [ + "", + "जनवरी", + "फरवरी", + "मार्च", + "अप्रैल ", + "मई", + "जून", + "जुलाई", + "अगस्त", + "सितंबर", + "अक्टूबर", + "नवंबर", + "दिसंबर", + ] + month_abbreviations = [ + "", + "जन", + "फ़र", + "मार्च", + "अप्रै", + "मई", + "जून", + "जुलाई", + "आग", + "सित", + "अकत", + "नवे", + "दिस", + ] + + day_names = [ + "", + "सोमवार", + "मंगलवार", + "बुधवार", + "गुरुवार", + "शुक्रवार", + "शनिवार", + "रविवार", + ] + day_abbreviations = ["", "सोम", "मंगल", "बुध", "गुरुवार", "शुक्र", "शनि", "रवि"] - day_names = ['', 'सोमवार', 'मंगलवार', 'बुधवार', 'गुरुवार', 'शुक्रवार', 'शनिवार', 'रविवार'] - day_abbreviations = ['', 'सोम', 'मंगल', 'बुध', 'गुरुवार', 'शुक्र', 'शनि', 'रवि'] class CzechLocale(Locale): - names = ['cs', 'cs_cz'] + names = ["cs", "cs-cz"] - timeframes = { - 'now': 'Teď', - 'seconds': { - 'past': '{0} sekundami', - 'future': ['{0} sekundy', '{0} sekund'] + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "Teď", + "second": {"past": "vteřina", "future": "vteřina", "zero": "vteřina"}, + "seconds": { + "past": "{0} sekundami", + "future-singular": "{0} sekundy", + "future-paucal": "{0} sekund", }, - 'minute': {'past': 'minutou', 'future': 'minutu', 'zero': '{0} minut'}, - 'minutes': { - 'past': '{0} minutami', - 'future': ['{0} minuty', '{0} minut'] + "minute": {"past": "minutou", "future": "minutu", "zero": "{0} minut"}, + "minutes": { + "past": "{0} minutami", + "future-singular": "{0} minuty", + "future-paucal": "{0} minut", }, - 'hour': {'past': 'hodinou', 'future': 'hodinu', 'zero': '{0} hodin'}, - 'hours': { - 'past': '{0} hodinami', - 'future': ['{0} hodiny', '{0} hodin'] + "hour": {"past": "hodinou", "future": "hodinu", "zero": "{0} hodin"}, + "hours": { + "past": "{0} hodinami", + "future-singular": "{0} hodiny", + "future-paucal": "{0} hodin", }, - 'day': {'past': 'dnem', 'future': 'den', 'zero': '{0} dnů'}, - 'days': { - 'past': '{0} dny', - 'future': ['{0} dny', '{0} dnů'] + "day": {"past": "dnem", "future": "den", "zero": "{0} dnů"}, + "days": { + "past": "{0} dny", + "future-singular": "{0} dny", + "future-paucal": "{0} dnů", }, - 'month': {'past': 'měsícem', 'future': 'měsíc', 'zero': '{0} měsíců'}, - 'months': { - 'past': '{0} měsíci', - 'future': ['{0} měsíce', '{0} měsíců'] + "week": {"past": "týdnem", "future": "týden", "zero": "{0} týdnů"}, + "weeks": { + "past": "{0} týdny", + "future-singular": "{0} týdny", + "future-paucal": "{0} týdnů", + }, + "month": {"past": "měsícem", "future": "měsíc", "zero": "{0} měsíců"}, + "months": { + "past": "{0} měsíci", + "future-singular": "{0} měsíce", + "future-paucal": "{0} měsíců", + }, + "year": {"past": "rokem", "future": "rok", "zero": "{0} let"}, + "years": { + "past": "{0} lety", + "future-singular": "{0} roky", + "future-paucal": "{0} let", }, - 'year': {'past': 'rokem', 'future': 'rok', 'zero': '{0} let'}, - 'years': { - 'past': '{0} lety', - 'future': ['{0} roky', '{0} let'] - } } - past = 'Před {0}' - future = 'Za {0}' + past = "Před {0}" + future = "Za {0}" - month_names = ['', 'leden', 'únor', 'březen', 'duben', 'květen', 'červen', - 'červenec', 'srpen', 'září', 'říjen', 'listopad', 'prosinec'] - month_abbreviations = ['', 'led', 'úno', 'bře', 'dub', 'kvě', 'čvn', 'čvc', - 'srp', 'zář', 'říj', 'lis', 'pro'] + month_names = [ + "", + "leden", + "únor", + "březen", + "duben", + "květen", + "červen", + "červenec", + "srpen", + "září", + "říjen", + "listopad", + "prosinec", + ] + month_abbreviations = [ + "", + "led", + "úno", + "bře", + "dub", + "kvě", + "čvn", + "čvc", + "srp", + "zář", + "říj", + "lis", + "pro", + ] - day_names = ['', 'pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', - 'sobota', 'neděle'] - day_abbreviations = ['', 'po', 'út', 'st', 'čt', 'pá', 'so', 'ne'] + day_names = [ + "", + "pondělí", + "úterý", + "středa", + "čtvrtek", + "pátek", + "sobota", + "neděle", + ] + day_abbreviations = ["", "po", "út", "st", "čt", "pá", "so", "ne"] - - def _format_timeframe(self, timeframe, delta): - '''Czech aware time frame format function, takes into account - the differences between past and future forms.''' + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + """Czech aware time frame format function, takes into account + the differences between past and future forms.""" + abs_delta = abs(delta) form = self.timeframes[timeframe] - if isinstance(form, dict): - if delta == 0: - form = form['zero'] # And *never* use 0 in the singular! - elif delta > 0: - form = form['future'] - else: - form = form['past'] - delta = abs(delta) - if isinstance(form, list): - if 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): - form = form[0] - else: - form = form[1] + if isinstance(form, str): + return form.format(abs_delta) - return form.format(delta) + if delta == 0: + key = "zero" # And *never* use 0 in the singular! + elif delta < 0: + key = "past" + else: + # Needed since both regular future and future-singular and future-paucal cases + if "future-singular" not in form: + key = "future" + elif 2 <= abs_delta % 10 <= 4 and ( + abs_delta % 100 < 10 or abs_delta % 100 >= 20 + ): + key = "future-singular" + else: + key = "future-paucal" + + form: str = form[key] + return form.format(abs_delta) class SlovakLocale(Locale): - names = ['sk', 'sk_sk'] + names = ["sk", "sk-sk"] - timeframes = { - 'now': 'Teraz', - 'seconds': { - 'past': 'pár sekundami', - 'future': ['{0} sekundy', '{0} sekúnd'] + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "Teraz", + "second": {"past": "sekundou", "future": "sekundu", "zero": "{0} sekúnd"}, + "seconds": { + "past": "{0} sekundami", + "future-singular": "{0} sekundy", + "future-paucal": "{0} sekúnd", }, - 'minute': {'past': 'minútou', 'future': 'minútu', 'zero': '{0} minút'}, - 'minutes': { - 'past': '{0} minútami', - 'future': ['{0} minúty', '{0} minút'] + "minute": {"past": "minútou", "future": "minútu", "zero": "{0} minút"}, + "minutes": { + "past": "{0} minútami", + "future-singular": "{0} minúty", + "future-paucal": "{0} minút", }, - 'hour': {'past': 'hodinou', 'future': 'hodinu', 'zero': '{0} hodín'}, - 'hours': { - 'past': '{0} hodinami', - 'future': ['{0} hodiny', '{0} hodín'] + "hour": {"past": "hodinou", "future": "hodinu", "zero": "{0} hodín"}, + "hours": { + "past": "{0} hodinami", + "future-singular": "{0} hodiny", + "future-paucal": "{0} hodín", }, - 'day': {'past': 'dňom', 'future': 'deň', 'zero': '{0} dní'}, - 'days': { - 'past': '{0} dňami', - 'future': ['{0} dni', '{0} dní'] + "day": {"past": "dňom", "future": "deň", "zero": "{0} dní"}, + "days": { + "past": "{0} dňami", + "future-singular": "{0} dni", + "future-paucal": "{0} dní", }, - 'month': {'past': 'mesiacom', 'future': 'mesiac', 'zero': '{0} mesiacov'}, - 'months': { - 'past': '{0} mesiacmi', - 'future': ['{0} mesiace', '{0} mesiacov'] + "week": {"past": "týždňom", "future": "týždeň", "zero": "{0} týždňov"}, + "weeks": { + "past": "{0} týždňami", + "future-singular": "{0} týždne", + "future-paucal": "{0} týždňov", + }, + "month": {"past": "mesiacom", "future": "mesiac", "zero": "{0} mesiacov"}, + "months": { + "past": "{0} mesiacmi", + "future-singular": "{0} mesiace", + "future-paucal": "{0} mesiacov", + }, + "year": {"past": "rokom", "future": "rok", "zero": "{0} rokov"}, + "years": { + "past": "{0} rokmi", + "future-singular": "{0} roky", + "future-paucal": "{0} rokov", }, - 'year': {'past': 'rokom', 'future': 'rok', 'zero': '{0} rokov'}, - 'years': { - 'past': '{0} rokmi', - 'future': ['{0} roky', '{0} rokov'] - } } - past = 'Pred {0}' - future = 'O {0}' + past = "Pred {0}" + future = "O {0}" + and_word = "a" - month_names = ['', 'január', 'február', 'marec', 'apríl', 'máj', 'jún', - 'júl', 'august', 'september', 'október', 'november', 'december'] - month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'máj', 'jún', 'júl', - 'aug', 'sep', 'okt', 'nov', 'dec'] + month_names = [ + "", + "január", + "február", + "marec", + "apríl", + "máj", + "jún", + "júl", + "august", + "september", + "október", + "november", + "december", + ] + month_abbreviations = [ + "", + "jan", + "feb", + "mar", + "apr", + "máj", + "jún", + "júl", + "aug", + "sep", + "okt", + "nov", + "dec", + ] - day_names = ['', 'pondelok', 'utorok', 'streda', 'štvrtok', 'piatok', - 'sobota', 'nedeľa'] - day_abbreviations = ['', 'po', 'ut', 'st', 'št', 'pi', 'so', 'ne'] + day_names = [ + "", + "pondelok", + "utorok", + "streda", + "štvrtok", + "piatok", + "sobota", + "nedeľa", + ] + day_abbreviations = ["", "po", "ut", "st", "št", "pi", "so", "ne"] - - def _format_timeframe(self, timeframe, delta): - '''Slovak aware time frame format function, takes into account - the differences between past and future forms.''' + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + """Slovak aware time frame format function, takes into account + the differences between past and future forms.""" + abs_delta = abs(delta) form = self.timeframes[timeframe] - if isinstance(form, dict): - if delta == 0: - form = form['zero'] # And *never* use 0 in the singular! - elif delta > 0: - form = form['future'] - else: - form = form['past'] - delta = abs(delta) - if isinstance(form, list): - if 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): - form = form[0] - else: - form = form[1] + if isinstance(form, str): + return form.format(abs_delta) - return form.format(delta) + if delta == 0: + key = "zero" # And *never* use 0 in the singular! + elif delta < 0: + key = "past" + else: + if "future-singular" not in form: + key = "future" + elif 2 <= abs_delta % 10 <= 4 and ( + abs_delta % 100 < 10 or abs_delta % 100 >= 20 + ): + key = "future-singular" + else: + key = "future-paucal" + + form: str = form[key] + return form.format(abs_delta) class FarsiLocale(Locale): - names = ['fa', 'fa_ir'] + names = ["fa", "fa-ir"] - past = '{0} قبل' - future = 'در {0}' + past = "{0} قبل" + future = "در {0}" timeframes = { - 'now': 'اکنون', - 'seconds': 'ثانیه', - 'minute': 'یک دقیقه', - 'minutes': '{0} دقیقه', - 'hour': 'یک ساعت', - 'hours': '{0} ساعت', - 'day': 'یک روز', - 'days': '{0} روز', - 'month': 'یک ماه', - 'months': '{0} ماه', - 'year': 'یک سال', - 'years': '{0} سال', + "now": "اکنون", + "second": "یک لحظه", + "seconds": "{0} ثانیه", + "minute": "یک دقیقه", + "minutes": "{0} دقیقه", + "hour": "یک ساعت", + "hours": "{0} ساعت", + "day": "یک روز", + "days": "{0} روز", + "month": "یک ماه", + "months": "{0} ماه", + "year": "یک سال", + "years": "{0} سال", } meridians = { - 'am': 'قبل از ظهر', - 'pm': 'بعد از ظهر', - 'AM': 'قبل از ظهر', - 'PM': 'بعد از ظهر', + "am": "قبل از ظهر", + "pm": "بعد از ظهر", + "AM": "قبل از ظهر", + "PM": "بعد از ظهر", } - month_names = ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', - 'August', 'September', 'October', 'November', 'December'] - month_abbreviations = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', - 'Sep', 'Oct', 'Nov', 'Dec'] + month_names = [ + "", + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ] + month_abbreviations = [ + "", + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ] - day_names = ['', 'دو شنبه', 'سه شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه', 'شنبه', 'یکشنبه'] - day_abbreviations = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] - - -class MacedonianLocale(Locale): - names = ['mk', 'mk_mk'] - - past = 'пред {0}' - future = 'за {0}' - - timeframes = { - 'now': 'сега', - 'seconds': 'секунди', - 'minute': 'една минута', - 'minutes': '{0} минути', - 'hour': 'еден саат', - 'hours': '{0} саати', - 'day': 'еден ден', - 'days': '{0} дена', - 'month': 'еден месец', - 'months': '{0} месеци', - 'year': 'една година', - 'years': '{0} години', - } - - meridians = { - 'am': 'дп', - 'pm': 'пп', - 'AM': 'претпладне', - 'PM': 'попладне', - } - - month_names = ['', 'Јануари', 'Февруари', 'Март', 'Април', 'Мај', 'Јуни', 'Јули', 'Август', 'Септември', 'Октомври', - 'Ноември', 'Декември'] - month_abbreviations = ['', 'Јан.', ' Фев.', ' Мар.', ' Апр.', ' Мај', ' Јун.', ' Јул.', ' Авг.', ' Септ.', ' Окт.', - ' Ноем.', ' Декем.'] - - day_names = ['', 'Понеделник', ' Вторник', ' Среда', ' Четврток', ' Петок', ' Сабота', ' Недела'] - day_abbreviations = ['', 'Пон.', ' Вт.', ' Сре.', ' Чет.', ' Пет.', ' Саб.', ' Нед.'] + day_names = [ + "", + "دو شنبه", + "سه شنبه", + "چهارشنبه", + "پنجشنبه", + "جمعه", + "شنبه", + "یکشنبه", + ] + day_abbreviations = ["", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] class HebrewLocale(Locale): - names = ['he', 'he_IL'] + names = ["he", "he-il"] - past = 'לפני {0}' - future = 'בעוד {0}' + past = "לפני {0}" + future = "בעוד {0}" + and_word = "ו" - timeframes = { - 'now': 'הרגע', - 'seconds': 'שניות', - 'minute': 'דקה', - 'minutes': '{0} דקות', - 'hour': 'שעה', - 'hours': '{0} שעות', - '2-hours': 'שעתיים', - 'day': 'יום', - 'days': '{0} ימים', - '2-days': 'יומיים', - 'month': 'חודש', - 'months': '{0} חודשים', - '2-months': 'חודשיים', - 'year': 'שנה', - 'years': '{0} שנים', - '2-years': 'שנתיים', + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "הרגע", + "second": "שנייה", + "seconds": "{0} שניות", + "minute": "דקה", + "minutes": "{0} דקות", + "hour": "שעה", + "hours": {"2": "שעתיים", "general": "{0} שעות"}, + "day": "יום", + "days": {"2": "יומיים", "general": "{0} ימים"}, + "week": "שבוע", + "weeks": {"2": "שבועיים", "general": "{0} שבועות"}, + "month": "חודש", + "months": {"2": "חודשיים", "general": "{0} חודשים"}, + "year": "שנה", + "years": {"2": "שנתיים", "general": "{0} שנים"}, } meridians = { - 'am': 'לפנ"צ', - 'pm': 'אחר"צ', - 'AM': 'לפני הצהריים', - 'PM': 'אחרי הצהריים', + "am": 'לפנ"צ', + "pm": 'אחר"צ', + "AM": "לפני הצהריים", + "PM": "אחרי הצהריים", } - month_names = ['', 'ינואר', 'פברואר', 'מרץ', 'אפריל', 'מאי', 'יוני', 'יולי', - 'אוגוסט', 'ספטמבר', 'אוקטובר', 'נובמבר', 'דצמבר'] - month_abbreviations = ['', 'ינו׳', 'פבר׳', 'מרץ', 'אפר׳', 'מאי', 'יוני', 'יולי', 'אוג׳', - 'ספט׳', 'אוק׳', 'נוב׳', 'דצמ׳'] + month_names = [ + "", + "ינואר", + "פברואר", + "מרץ", + "אפריל", + "מאי", + "יוני", + "יולי", + "אוגוסט", + "ספטמבר", + "אוקטובר", + "נובמבר", + "דצמבר", + ] + month_abbreviations = [ + "", + "ינו׳", + "פבר׳", + "מרץ", + "אפר׳", + "מאי", + "יוני", + "יולי", + "אוג׳", + "ספט׳", + "אוק׳", + "נוב׳", + "דצמ׳", + ] - day_names = ['', 'שני', 'שלישי', 'רביעי', 'חמישי', 'שישי', 'שבת', 'ראשון'] - day_abbreviations = ['', 'ב׳', 'ג׳', 'ד׳', 'ה׳', 'ו׳', 'ש׳', 'א׳'] + day_names = ["", "שני", "שלישי", "רביעי", "חמישי", "שישי", "שבת", "ראשון"] + day_abbreviations = ["", "ב׳", "ג׳", "ד׳", "ה׳", "ו׳", "ש׳", "א׳"] + + def _format_timeframe( + self, timeframe: TimeFrameLiteral, delta: Union[float, int] + ) -> str: + """Hebrew couple of aware""" + form = self.timeframes[timeframe] + delta = abs(trunc(delta)) + + if isinstance(form, Mapping): + if delta == 2: + form = form["2"] + else: + form = form["general"] + + return form.format(delta) + + def describe_multi( + self, + timeframes: Sequence[Tuple[TimeFrameLiteral, Union[int, float]]], + only_distance: bool = False, + ) -> str: + """Describes a delta within multiple timeframes in plain language. + In Hebrew, the and word behaves a bit differently. + + :param timeframes: a list of string, quantity pairs each representing a timeframe and delta. + :param only_distance: return only distance eg: "2 hours and 11 seconds" without "in" or "ago" keywords + """ + + humanized = "" + for index, (timeframe, delta) in enumerate(timeframes): + last_humanized = self._format_timeframe(timeframe, trunc(delta)) + if index == 0: + humanized = last_humanized + elif index == len(timeframes) - 1: # Must have at least 2 items + humanized += " " + self.and_word + if last_humanized[0].isdecimal(): + humanized += "־" + humanized += last_humanized + else: # Don't add for the last one + humanized += ", " + last_humanized + + if not only_distance: + humanized = self._format_relative(humanized, timeframe, trunc(delta)) + + return humanized - def _format_timeframe(self, timeframe, delta): - '''Hebrew couple of aware''' - couple = '2-{0}'.format(timeframe) - if abs(delta) == 2 and couple in self.timeframes: - return self.timeframes[couple].format(abs(delta)) - else: - return self.timeframes[timeframe].format(abs(delta)) class MarathiLocale(Locale): - names = ['mr'] + names = ["mr"] - past = '{0} आधी' - future = '{0} नंतर' + past = "{0} आधी" + future = "{0} नंतर" timeframes = { - 'now': 'सद्य', - 'seconds': 'सेकंद', - 'minute': 'एक मिनिट ', - 'minutes': '{0} मिनिट ', - 'hour': 'एक तास', - 'hours': '{0} तास', - 'day': 'एक दिवस', - 'days': '{0} दिवस', - 'month': 'एक महिना ', - 'months': '{0} महिने ', - 'year': 'एक वर्ष ', - 'years': '{0} वर्ष ', + "now": "सद्य", + "second": "एक सेकंद", + "seconds": "{0} सेकंद", + "minute": "एक मिनिट ", + "minutes": "{0} मिनिट ", + "hour": "एक तास", + "hours": "{0} तास", + "day": "एक दिवस", + "days": "{0} दिवस", + "month": "एक महिना ", + "months": "{0} महिने ", + "year": "एक वर्ष ", + "years": "{0} वर्ष ", } - meridians = { - 'am': 'सकाळ', - 'pm': 'संध्याकाळ', - 'AM': 'सकाळ', - 'PM': 'संध्याकाळ', - } + meridians = {"am": "सकाळ", "pm": "संध्याकाळ", "AM": "सकाळ", "PM": "संध्याकाळ"} - month_names = ['', 'जानेवारी', 'फेब्रुवारी', 'मार्च', 'एप्रिल', 'मे', 'जून', 'जुलै', - 'अॉगस्ट', 'सप्टेंबर', 'अॉक्टोबर', 'नोव्हेंबर', 'डिसेंबर'] - month_abbreviations = ['', 'जान', 'फेब्रु', 'मार्च', 'एप्रि', 'मे', 'जून', 'जुलै', 'अॉग', - 'सप्टें', 'अॉक्टो', 'नोव्हें', 'डिसें'] + month_names = [ + "", + "जानेवारी", + "फेब्रुवारी", + "मार्च", + "एप्रिल", + "मे", + "जून", + "जुलै", + "अॉगस्ट", + "सप्टेंबर", + "अॉक्टोबर", + "नोव्हेंबर", + "डिसेंबर", + ] + month_abbreviations = [ + "", + "जान", + "फेब्रु", + "मार्च", + "एप्रि", + "मे", + "जून", + "जुलै", + "अॉग", + "सप्टें", + "अॉक्टो", + "नोव्हें", + "डिसें", + ] - day_names = ['', 'सोमवार', 'मंगळवार', 'बुधवार', 'गुरुवार', 'शुक्रवार', 'शनिवार', 'रविवार'] - day_abbreviations = ['', 'सोम', 'मंगळ', 'बुध', 'गुरु', 'शुक्र', 'शनि', 'रवि'] + day_names = [ + "", + "सोमवार", + "मंगळवार", + "बुधवार", + "गुरुवार", + "शुक्रवार", + "शनिवार", + "रविवार", + ] + day_abbreviations = ["", "सोम", "मंगळ", "बुध", "गुरु", "शुक्र", "शनि", "रवि"] -def _map_locales(): - - locales = {} - - for cls_name, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): - if issubclass(cls, Locale): - for name in cls.names: - locales[name.lower()] = cls - - return locales class CatalanLocale(Locale): - names = ['ca', 'ca_es', 'ca_ad', 'ca_fr', 'ca_it'] - past = 'Fa {0}' - future = 'En {0}' + names = ["ca", "ca-es", "ca-ad", "ca-fr", "ca-it"] + past = "Fa {0}" + future = "En {0}" + and_word = "i" timeframes = { - 'now': 'Ara mateix', - 'seconds': 'segons', - 'minute': '1 minut', - 'minutes': '{0} minuts', - 'hour': 'una hora', - 'hours': '{0} hores', - 'day': 'un dia', - 'days': '{0} dies', - 'month': 'un mes', - 'months': '{0} mesos', - 'year': 'un any', - 'years': '{0} anys', + "now": "Ara mateix", + "second": "un segon", + "seconds": "{0} segons", + "minute": "un minut", + "minutes": "{0} minuts", + "hour": "una hora", + "hours": "{0} hores", + "day": "un dia", + "days": "{0} dies", + "month": "un mes", + "months": "{0} mesos", + "year": "un any", + "years": "{0} anys", } - month_names = ['', 'Gener', 'Febrer', 'Març', 'Abril', 'Maig', 'Juny', 'Juliol', 'Agost', 'Setembre', 'Octubre', 'Novembre', 'Desembre'] - month_abbreviations = ['', 'Gener', 'Febrer', 'Març', 'Abril', 'Maig', 'Juny', 'Juliol', 'Agost', 'Setembre', 'Octubre', 'Novembre', 'Desembre'] - day_names = ['', 'Dilluns', 'Dimarts', 'Dimecres', 'Dijous', 'Divendres', 'Dissabte', 'Diumenge'] - day_abbreviations = ['', 'Dilluns', 'Dimarts', 'Dimecres', 'Dijous', 'Divendres', 'Dissabte', 'Diumenge'] + month_names = [ + "", + "gener", + "febrer", + "març", + "abril", + "maig", + "juny", + "juliol", + "agost", + "setembre", + "octubre", + "novembre", + "desembre", + ] + month_abbreviations = [ + "", + "gen.", + "febr.", + "març", + "abr.", + "maig", + "juny", + "jul.", + "ag.", + "set.", + "oct.", + "nov.", + "des.", + ] + day_names = [ + "", + "dilluns", + "dimarts", + "dimecres", + "dijous", + "divendres", + "dissabte", + "diumenge", + ] + day_abbreviations = [ + "", + "dl.", + "dt.", + "dc.", + "dj.", + "dv.", + "ds.", + "dg.", + ] + class BasqueLocale(Locale): - names = ['eu', 'eu_eu'] - past = 'duela {0}' - future = '{0}' # I don't know what's the right phrase in Basque for the future. + names = ["eu", "eu-eu"] + past = "duela {0}" + future = "{0}" # I don't know what's the right phrase in Basque for the future. timeframes = { - 'now': 'Orain', - 'seconds': 'segundu', - 'minute': 'minutu bat', - 'minutes': '{0} minutu', - 'hour': 'ordu bat', - 'hours': '{0} ordu', - 'day': 'egun bat', - 'days': '{0} egun', - 'month': 'hilabete bat', - 'months': '{0} hilabet', - 'year': 'urte bat', - 'years': '{0} urte', + "now": "Orain", + "second": "segundo bat", + "seconds": "{0} segundu", + "minute": "minutu bat", + "minutes": "{0} minutu", + "hour": "ordu bat", + "hours": "{0} ordu", + "day": "egun bat", + "days": "{0} egun", + "month": "hilabete bat", + "months": "{0} hilabet", + "year": "urte bat", + "years": "{0} urte", } - month_names = ['', 'urtarrilak', 'otsailak', 'martxoak', 'apirilak', 'maiatzak', 'ekainak', 'uztailak', 'abuztuak', 'irailak', 'urriak', 'azaroak', 'abenduak'] - month_abbreviations = ['', 'urt', 'ots', 'mar', 'api', 'mai', 'eka', 'uzt', 'abu', 'ira', 'urr', 'aza', 'abe'] - day_names = ['', 'asteleehna', 'asteartea', 'asteazkena', 'osteguna', 'ostirala', 'larunbata', 'igandea'] - day_abbreviations = ['', 'al', 'ar', 'az', 'og', 'ol', 'lr', 'ig'] + month_names = [ + "", + "urtarrilak", + "otsailak", + "martxoak", + "apirilak", + "maiatzak", + "ekainak", + "uztailak", + "abuztuak", + "irailak", + "urriak", + "azaroak", + "abenduak", + ] + month_abbreviations = [ + "", + "urt", + "ots", + "mar", + "api", + "mai", + "eka", + "uzt", + "abu", + "ira", + "urr", + "aza", + "abe", + ] + day_names = [ + "", + "astelehena", + "asteartea", + "asteazkena", + "osteguna", + "ostirala", + "larunbata", + "igandea", + ] + day_abbreviations = ["", "al", "ar", "az", "og", "ol", "lr", "ig"] class HungarianLocale(Locale): - names = ['hu', 'hu_hu'] + names = ["hu", "hu-hu"] - past = '{0} ezelőtt' - future = '{0} múlva' + past = "{0} ezelőtt" + future = "{0} múlva" - timeframes = { - 'now': 'éppen most', - 'seconds': { - 'past': 'másodpercekkel', - 'future': 'pár másodperc' - }, - 'minute': {'past': 'egy perccel', 'future': 'egy perc'}, - 'minutes': {'past': '{0} perccel', 'future': '{0} perc'}, - 'hour': {'past': 'egy órával', 'future': 'egy óra'}, - 'hours': {'past': '{0} órával', 'future': '{0} óra'}, - 'day': { - 'past': 'egy nappal', - 'future': 'egy nap' - }, - 'days': { - 'past': '{0} nappal', - 'future': '{0} nap' - }, - 'month': {'past': 'egy hónappal', 'future': 'egy hónap'}, - 'months': {'past': '{0} hónappal', 'future': '{0} hónap'}, - 'year': {'past': 'egy évvel', 'future': 'egy év'}, - 'years': {'past': '{0} évvel', 'future': '{0} év'}, + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "éppen most", + "second": {"past": "egy második", "future": "egy második"}, + "seconds": {"past": "{0} másodpercekkel", "future": "{0} pár másodperc"}, + "minute": {"past": "egy perccel", "future": "egy perc"}, + "minutes": {"past": "{0} perccel", "future": "{0} perc"}, + "hour": {"past": "egy órával", "future": "egy óra"}, + "hours": {"past": "{0} órával", "future": "{0} óra"}, + "day": {"past": "egy nappal", "future": "egy nap"}, + "days": {"past": "{0} nappal", "future": "{0} nap"}, + "month": {"past": "egy hónappal", "future": "egy hónap"}, + "months": {"past": "{0} hónappal", "future": "{0} hónap"}, + "year": {"past": "egy évvel", "future": "egy év"}, + "years": {"past": "{0} évvel", "future": "{0} év"}, } - month_names = ['', 'január', 'február', 'március', 'április', 'május', - 'június', 'július', 'augusztus', 'szeptember', - 'október', 'november', 'december'] - month_abbreviations = ['', 'jan', 'febr', 'márc', 'ápr', 'máj', 'jún', - 'júl', 'aug', 'szept', 'okt', 'nov', 'dec'] + month_names = [ + "", + "január", + "február", + "március", + "április", + "május", + "június", + "július", + "augusztus", + "szeptember", + "október", + "november", + "december", + ] + month_abbreviations = [ + "", + "jan", + "febr", + "márc", + "ápr", + "máj", + "jún", + "júl", + "aug", + "szept", + "okt", + "nov", + "dec", + ] - day_names = ['', 'hétfő', 'kedd', 'szerda', 'csütörtök', 'péntek', - 'szombat', 'vasárnap'] - day_abbreviations = ['', 'hét', 'kedd', 'szer', 'csüt', 'pént', - 'szom', 'vas'] + day_names = [ + "", + "hétfő", + "kedd", + "szerda", + "csütörtök", + "péntek", + "szombat", + "vasárnap", + ] + day_abbreviations = ["", "hét", "kedd", "szer", "csüt", "pént", "szom", "vas"] - meridians = { - 'am': 'de', - 'pm': 'du', - 'AM': 'DE', - 'PM': 'DU', - } + meridians = {"am": "de", "pm": "du", "AM": "DE", "PM": "DU"} - def _format_timeframe(self, timeframe, delta): + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: form = self.timeframes[timeframe] - if isinstance(form, dict): + if isinstance(form, Mapping): if delta > 0: - form = form['future'] + form = form["future"] else: - form = form['past'] + form = form["past"] return form.format(abs(delta)) class EsperantoLocale(Locale): - names = ['eo', 'eo_xx'] - past = 'antaŭ {0}' - future = 'post {0}' + names = ["eo", "eo-xx"] + past = "antaŭ {0}" + future = "post {0}" timeframes = { - 'now': 'nun', - 'seconds': 'kelkaj sekundoj', - 'minute': 'unu minuto', - 'minutes': '{0} minutoj', - 'hour': 'un horo', - 'hours': '{0} horoj', - 'day': 'unu tago', - 'days': '{0} tagoj', - 'month': 'unu monato', - 'months': '{0} monatoj', - 'year': 'unu jaro', - 'years': '{0} jaroj', + "now": "nun", + "second": "sekundo", + "seconds": "{0} kelkaj sekundoj", + "minute": "unu minuto", + "minutes": "{0} minutoj", + "hour": "un horo", + "hours": "{0} horoj", + "day": "unu tago", + "days": "{0} tagoj", + "month": "unu monato", + "months": "{0} monatoj", + "year": "unu jaro", + "years": "{0} jaroj", } - month_names = ['', 'januaro', 'februaro', 'marto', 'aprilo', 'majo', - 'junio', 'julio', 'aŭgusto', 'septembro', 'oktobro', - 'novembro', 'decembro'] - month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'maj', 'jun', - 'jul', 'aŭg', 'sep', 'okt', 'nov', 'dec'] + month_names = [ + "", + "januaro", + "februaro", + "marto", + "aprilo", + "majo", + "junio", + "julio", + "aŭgusto", + "septembro", + "oktobro", + "novembro", + "decembro", + ] + month_abbreviations = [ + "", + "jan", + "feb", + "mar", + "apr", + "maj", + "jun", + "jul", + "aŭg", + "sep", + "okt", + "nov", + "dec", + ] - day_names = ['', 'lundo', 'mardo', 'merkredo', 'ĵaŭdo', 'vendredo', - 'sabato', 'dimanĉo'] - day_abbreviations = ['', 'lun', 'mar', 'mer', 'ĵaŭ', 'ven', - 'sab', 'dim'] + day_names = [ + "", + "lundo", + "mardo", + "merkredo", + "ĵaŭdo", + "vendredo", + "sabato", + "dimanĉo", + ] + day_abbreviations = ["", "lun", "mar", "mer", "ĵaŭ", "ven", "sab", "dim"] - meridians = { - 'am': 'atm', - 'pm': 'ptm', - 'AM': 'ATM', - 'PM': 'PTM', - } + meridians = {"am": "atm", "pm": "ptm", "AM": "ATM", "PM": "PTM"} - ordinal_day_re = r'((?P[1-3]?[0-9](?=a))a)' + ordinal_day_re = r"((?P[1-3]?[0-9](?=a))a)" - def _ordinal_number(self, n): - return '{0}a'.format(n) + def _ordinal_number(self, n: int) -> str: + return f"{n}a" class ThaiLocale(Locale): - names = ['th', 'th_th'] + names = ["th", "th-th"] - past = '{0}{1}ที่ผ่านมา' - future = 'ในอีก{1}{0}' + past = "{0} ที่ผ่านมา" + future = "ในอีก {0}" timeframes = { - 'now': 'ขณะนี้', - 'seconds': 'ไม่กี่วินาที', - 'minute': '1 นาที', - 'minutes': '{0} นาที', - 'hour': '1 ชั่วโมง', - 'hours': '{0} ชั่วโมง', - 'day': '1 วัน', - 'days': '{0} วัน', - 'month': '1 เดือน', - 'months': '{0} เดือน', - 'year': '1 ปี', - 'years': '{0} ปี', + "now": "ขณะนี้", + "second": "วินาที", + "seconds": "{0} ไม่กี่วินาที", + "minute": "1 นาที", + "minutes": "{0} นาที", + "hour": "1 ชั่วโมง", + "hours": "{0} ชั่วโมง", + "day": "1 วัน", + "days": "{0} วัน", + "month": "1 เดือน", + "months": "{0} เดือน", + "year": "1 ปี", + "years": "{0} ปี", } - month_names = ['', 'มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', - 'พฤษภาคม', 'มิถุนายน', 'กรกฏาคม', 'สิงหาคม', - 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'] - month_abbreviations = ['', 'ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', - 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', - 'พ.ย.', 'ธ.ค.'] + month_names = [ + "", + "มกราคม", + "กุมภาพันธ์", + "มีนาคม", + "เมษายน", + "พฤษภาคม", + "มิถุนายน", + "กรกฎาคม", + "สิงหาคม", + "กันยายน", + "ตุลาคม", + "พฤศจิกายน", + "ธันวาคม", + ] + month_abbreviations = [ + "", + "ม.ค.", + "ก.พ.", + "มี.ค.", + "เม.ย.", + "พ.ค.", + "มิ.ย.", + "ก.ค.", + "ส.ค.", + "ก.ย.", + "ต.ค.", + "พ.ย.", + "ธ.ค.", + ] - day_names = ['', 'จันทร์', 'อังคาร', 'พุธ', 'พฤหัสบดี', 'ศุกร์', - 'เสาร์', 'อาทิตย์'] - day_abbreviations = ['', 'จ', 'อ', 'พ', 'พฤ', 'ศ', 'ส', 'อา'] + day_names = ["", "จันทร์", "อังคาร", "พุธ", "พฤหัสบดี", "ศุกร์", "เสาร์", "อาทิตย์"] + day_abbreviations = ["", "จ", "อ", "พ", "พฤ", "ศ", "ส", "อา"] - meridians = { - 'am': 'am', - 'pm': 'pm', - 'AM': 'AM', - 'PM': 'PM', - } + meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"} BE_OFFSET = 543 - def year_full(self, year): - '''Thai always use Buddhist Era (BE) which is CE + 543''' + def year_full(self, year: int) -> str: + """Thai always use Buddhist Era (BE) which is CE + 543""" year += self.BE_OFFSET - return '{0:04d}'.format(year) + return f"{year:04d}" - def year_abbreviation(self, year): - '''Thai always use Buddhist Era (BE) which is CE + 543''' + def year_abbreviation(self, year: int) -> str: + """Thai always use Buddhist Era (BE) which is CE + 543""" year += self.BE_OFFSET - return '{0:04d}'.format(year)[2:] + return f"{year:04d}"[2:] - def _format_relative(self, humanized, timeframe, delta): - '''Thai normally doesn't have any space between words''' - if timeframe == 'now': + def _format_relative( + self, + humanized: str, + timeframe: TimeFrameLiteral, + delta: Union[float, int], + ) -> str: + """Thai normally doesn't have any space between words""" + if timeframe == "now": return humanized - space = '' if timeframe == 'seconds' else ' ' + direction = self.past if delta < 0 else self.future + relative_string = direction.format(humanized) - return direction.format(humanized, space) + if timeframe == "seconds": + relative_string = relative_string.replace(" ", "") + return relative_string class BengaliLocale(Locale): - names = ['bn', 'bn_bd', 'bn_in'] + names = ["bn", "bn-bd", "bn-in"] - past = '{0} আগে' - future = '{0} পরে' + past = "{0} আগে" + future = "{0} পরে" timeframes = { - 'now': 'এখন', - 'seconds': 'সেকেন্ড', - 'minute': 'এক মিনিট', - 'minutes': '{0} মিনিট', - 'hour': 'এক ঘণ্টা', - 'hours': '{0} ঘণ্টা', - 'day': 'এক দিন', - 'days': '{0} দিন', - 'month': 'এক মাস', - 'months': '{0} মাস ', - 'year': 'এক বছর', - 'years': '{0} বছর', + "now": "এখন", + "second": "একটি দ্বিতীয়", + "seconds": "{0} সেকেন্ড", + "minute": "এক মিনিট", + "minutes": "{0} মিনিট", + "hour": "এক ঘণ্টা", + "hours": "{0} ঘণ্টা", + "day": "এক দিন", + "days": "{0} দিন", + "month": "এক মাস", + "months": "{0} মাস ", + "year": "এক বছর", + "years": "{0} বছর", } - meridians = { - 'am': 'সকাল', - 'pm': 'বিকাল', - 'AM': 'সকাল', - 'PM': 'বিকাল', - } + meridians = {"am": "সকাল", "pm": "বিকাল", "AM": "সকাল", "PM": "বিকাল"} - month_names = ['', 'জানুয়ারি', 'ফেব্রুয়ারি', 'মার্চ', 'এপ্রিল', 'মে', 'জুন', 'জুলাই', - 'আগস্ট', 'সেপ্টেম্বর', 'অক্টোবর', 'নভেম্বর', 'ডিসেম্বর'] - month_abbreviations = ['', 'জানু', 'ফেব', 'মার্চ', 'এপ্রি', 'মে', 'জুন', 'জুল', - 'অগা','সেপ্ট', 'অক্টো', 'নভে', 'ডিসে'] + month_names = [ + "", + "জানুয়ারি", + "ফেব্রুয়ারি", + "মার্চ", + "এপ্রিল", + "মে", + "জুন", + "জুলাই", + "আগস্ট", + "সেপ্টেম্বর", + "অক্টোবর", + "নভেম্বর", + "ডিসেম্বর", + ] + month_abbreviations = [ + "", + "জানু", + "ফেব", + "মার্চ", + "এপ্রি", + "মে", + "জুন", + "জুল", + "অগা", + "সেপ্ট", + "অক্টো", + "নভে", + "ডিসে", + ] - day_names = ['', 'সোমবার', 'মঙ্গলবার', 'বুধবার', 'বৃহস্পতিবার', 'শুক্রবার', 'শনিবার', 'রবিবার'] - day_abbreviations = ['', 'সোম', 'মঙ্গল', 'বুধ', 'বৃহঃ', 'শুক্র', 'শনি', 'রবি'] + day_names = [ + "", + "সোমবার", + "মঙ্গলবার", + "বুধবার", + "বৃহস্পতিবার", + "শুক্রবার", + "শনিবার", + "রবিবার", + ] + day_abbreviations = ["", "সোম", "মঙ্গল", "বুধ", "বৃহঃ", "শুক্র", "শনি", "রবি"] - def _ordinal_number(self, n): + def _ordinal_number(self, n: int) -> str: if n > 10 or n == 0: - return '{0}তম'.format(n) + return f"{n}তম" if n in [1, 5, 7, 8, 9, 10]: - return '{0}ম'.format(n) + return f"{n}ম" if n in [2, 3]: - return '{0}য়'.format(n) + return f"{n}য়" if n == 4: - return '{0}র্থ'.format(n) + return f"{n}র্থ" if n == 6: - return '{0}ষ্ঠ'.format(n) + return f"{n}ষ্ঠ" class RomanshLocale(Locale): - names = ['rm', 'rm_ch'] + names = ["rm", "rm-ch"] - past = 'avant {0}' - future = 'en {0}' + past = "avant {0}" + future = "en {0}" timeframes = { - 'now': 'en quest mument', - 'seconds': 'secundas', - 'minute': 'ina minuta', - 'minutes': '{0} minutas', - 'hour': 'in\'ura', - 'hours': '{0} ura', - 'day': 'in di', - 'days': '{0} dis', - 'month': 'in mais', - 'months': '{0} mais', - 'year': 'in onn', - 'years': '{0} onns', + "now": "en quest mument", + "second": "in secunda", + "seconds": "{0} secundas", + "minute": "ina minuta", + "minutes": "{0} minutas", + "hour": "in'ura", + "hours": "{0} ura", + "day": "in di", + "days": "{0} dis", + "month": "in mais", + "months": "{0} mais", + "year": "in onn", + "years": "{0} onns", } month_names = [ - '', 'schaner', 'favrer', 'mars', 'avrigl', 'matg', 'zercladur', - 'fanadur', 'avust', 'settember', 'october', 'november', 'december' + "", + "schaner", + "favrer", + "mars", + "avrigl", + "matg", + "zercladur", + "fanadur", + "avust", + "settember", + "october", + "november", + "december", ] month_abbreviations = [ - '', 'schan', 'fav', 'mars', 'avr', 'matg', 'zer', 'fan', 'avu', - 'set', 'oct', 'nov', 'dec' + "", + "schan", + "fav", + "mars", + "avr", + "matg", + "zer", + "fan", + "avu", + "set", + "oct", + "nov", + "dec", ] day_names = [ - '', 'glindesdi', 'mardi', 'mesemna', 'gievgia', 'venderdi', - 'sonda', 'dumengia' + "", + "glindesdi", + "mardi", + "mesemna", + "gievgia", + "venderdi", + "sonda", + "dumengia", ] - day_abbreviations = [ - '', 'gli', 'ma', 'me', 'gie', 've', 'so', 'du' - ] - - -class SwissLocale(Locale): - - names = ['de', 'de_ch'] - - past = 'vor {0}' - future = 'in {0}' - - timeframes = { - 'now': 'gerade eben', - 'seconds': 'Sekunden', - 'minute': 'einer Minute', - 'minutes': '{0} Minuten', - 'hour': 'einer Stunde', - 'hours': '{0} Stunden', - 'day': 'einem Tag', - 'days': '{0} Tage', - 'month': 'einem Monat', - 'months': '{0} Monaten', - 'year': 'einem Jahr', - 'years': '{0} Jahren', - } - - month_names = [ - '', 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', - 'August', 'September', 'Oktober', 'November', 'Dezember' - ] - - month_abbreviations = [ - '', 'Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', - 'Okt', 'Nov', 'Dez' - ] - - day_names = [ - '', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', - 'Samstag', 'Sonntag' - ] - - day_abbreviations = [ - '', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So' - ] + day_abbreviations = ["", "gli", "ma", "me", "gie", "ve", "so", "du"] class RomanianLocale(Locale): - names = ['ro', 'ro_ro'] + names = ["ro", "ro-ro"] - past = '{0} în urmă' - future = 'peste {0}' + past = "{0} în urmă" + future = "peste {0}" + and_word = "și" timeframes = { - 'now': 'acum', - 'seconds': 'câteva secunde', - 'minute': 'un minut', - 'minutes': '{0} minute', - 'hour': 'o oră', - 'hours': '{0} ore', - 'day': 'o zi', - 'days': '{0} zile', - 'month': 'o lună', - 'months': '{0} luni', - 'year': 'un an', - 'years': '{0} ani', - } - - month_names = ['', 'ianuarie', 'februarie', 'martie', 'aprilie', 'mai', 'iunie', 'iulie', - 'august', 'septembrie', 'octombrie', 'noiembrie', 'decembrie'] - month_abbreviations = ['', 'ian', 'febr', 'mart', 'apr', 'mai', 'iun', 'iul', 'aug', 'sept', 'oct', 'nov', 'dec'] - - day_names = ['', 'luni', 'marți', 'miercuri', 'joi', 'vineri', 'sâmbătă', 'duminică'] - day_abbreviations = ['', 'Lun', 'Mar', 'Mie', 'Joi', 'Vin', 'Sâm', 'Dum'] - - -class SlovenianLocale(Locale): - names = ['sl', 'sl_si'] - - past = 'pred {0}' - future = 'čez {0}' - - timeframes = { - 'now': 'zdaj', - 'seconds': 'sekund', - 'minute': 'minuta', - 'minutes': '{0} minutami', - 'hour': 'uro', - 'hours': '{0} ur', - 'day': 'dan', - 'days': '{0} dni', - 'month': 'mesec', - 'months': '{0} mesecev', - 'year': 'leto', - 'years': '{0} let', - } - - meridians = { - 'am': '', - 'pm': '', - 'AM': '', - 'PM': '', + "now": "acum", + "second": "o secunda", + "seconds": "{0} câteva secunde", + "minute": "un minut", + "minutes": "{0} minute", + "hour": "o oră", + "hours": "{0} ore", + "day": "o zi", + "days": "{0} zile", + "month": "o lună", + "months": "{0} luni", + "year": "un an", + "years": "{0} ani", } month_names = [ - '', 'Januar', 'Februar', 'Marec', 'April', 'Maj', 'Junij', 'Julij', - 'Avgust', 'September', 'Oktober', 'November', 'December' + "", + "ianuarie", + "februarie", + "martie", + "aprilie", + "mai", + "iunie", + "iulie", + "august", + "septembrie", + "octombrie", + "noiembrie", + "decembrie", ] - month_abbreviations = [ - '', 'Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Avg', - 'Sep', 'Okt', 'Nov', 'Dec' + "", + "ian", + "febr", + "mart", + "apr", + "mai", + "iun", + "iul", + "aug", + "sept", + "oct", + "nov", + "dec", ] day_names = [ - '', 'Ponedeljek', 'Torek', 'Sreda', 'Četrtek', 'Petek', 'Sobota', 'Nedelja' + "", + "luni", + "marți", + "miercuri", + "joi", + "vineri", + "sâmbătă", + "duminică", + ] + day_abbreviations = ["", "Lun", "Mar", "Mie", "Joi", "Vin", "Sâm", "Dum"] + + +class SlovenianLocale(Locale): + names = ["sl", "sl-si"] + + past = "pred {0}" + future = "čez {0}" + and_word = "in" + + timeframes = { + "now": "zdaj", + "second": "sekundo", + "seconds": "{0} sekund", + "minute": "minuta", + "minutes": "{0} minutami", + "hour": "uro", + "hours": "{0} ur", + "day": "dan", + "days": "{0} dni", + "month": "mesec", + "months": "{0} mesecev", + "year": "leto", + "years": "{0} let", + } + + meridians = {"am": "", "pm": "", "AM": "", "PM": ""} + + month_names = [ + "", + "Januar", + "Februar", + "Marec", + "April", + "Maj", + "Junij", + "Julij", + "Avgust", + "September", + "Oktober", + "November", + "December", + ] + + month_abbreviations = [ + "", + "Jan", + "Feb", + "Mar", + "Apr", + "Maj", + "Jun", + "Jul", + "Avg", + "Sep", + "Okt", + "Nov", + "Dec", + ] + + day_names = [ + "", + "Ponedeljek", + "Torek", + "Sreda", + "Četrtek", + "Petek", + "Sobota", + "Nedelja", + ] + + day_abbreviations = ["", "Pon", "Tor", "Sre", "Čet", "Pet", "Sob", "Ned"] + + +class IndonesianLocale(Locale): + + names = ["id", "id-id"] + + past = "{0} yang lalu" + future = "dalam {0}" + and_word = "dan" + + timeframes = { + "now": "baru saja", + "second": "1 sebentar", + "seconds": "{0} detik", + "minute": "1 menit", + "minutes": "{0} menit", + "hour": "1 jam", + "hours": "{0} jam", + "day": "1 hari", + "days": "{0} hari", + "month": "1 bulan", + "months": "{0} bulan", + "year": "1 tahun", + "years": "{0} tahun", + } + + meridians = {"am": "", "pm": "", "AM": "", "PM": ""} + + month_names = [ + "", + "Januari", + "Februari", + "Maret", + "April", + "Mei", + "Juni", + "Juli", + "Agustus", + "September", + "Oktober", + "November", + "Desember", + ] + + month_abbreviations = [ + "", + "Jan", + "Feb", + "Mar", + "Apr", + "Mei", + "Jun", + "Jul", + "Ags", + "Sept", + "Okt", + "Nov", + "Des", + ] + + day_names = ["", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Minggu"] + + day_abbreviations = [ + "", + "Senin", + "Selasa", + "Rabu", + "Kamis", + "Jumat", + "Sabtu", + "Minggu", + ] + + +class NepaliLocale(Locale): + names = ["ne", "ne-np"] + + past = "{0} पहिले" + future = "{0} पछी" + + timeframes = { + "now": "अहिले", + "second": "एक सेकेन्ड", + "seconds": "{0} सेकण्ड", + "minute": "मिनेट", + "minutes": "{0} मिनेट", + "hour": "एक घण्टा", + "hours": "{0} घण्टा", + "day": "एक दिन", + "days": "{0} दिन", + "month": "एक महिना", + "months": "{0} महिना", + "year": "एक बर्ष", + "years": "{0} बर्ष", + } + + meridians = {"am": "पूर्वाह्न", "pm": "अपरान्ह", "AM": "पूर्वाह्न", "PM": "अपरान्ह"} + + month_names = [ + "", + "जनवरी", + "फेब्रुअरी", + "मार्च", + "एप्रील", + "मे", + "जुन", + "जुलाई", + "अगष्ट", + "सेप्टेम्बर", + "अक्टोबर", + "नोवेम्बर", + "डिसेम्बर", + ] + month_abbreviations = [ + "", + "जन", + "फेब", + "मार्च", + "एप्रील", + "मे", + "जुन", + "जुलाई", + "अग", + "सेप", + "अक्ट", + "नोव", + "डिस", + ] + + day_names = [ + "", + "सोमवार", + "मंगलवार", + "बुधवार", + "बिहिवार", + "शुक्रवार", + "शनिवार", + "आइतवार", + ] + + day_abbreviations = ["", "सोम", "मंगल", "बुध", "बिहि", "शुक्र", "शनि", "आइत"] + + +class EstonianLocale(Locale): + names = ["ee", "et"] + + past = "{0} tagasi" + future = "{0} pärast" + and_word = "ja" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Mapping[str, str]]] = { + "now": {"past": "just nüüd", "future": "just nüüd"}, + "second": {"past": "üks sekund", "future": "ühe sekundi"}, + "seconds": {"past": "{0} sekundit", "future": "{0} sekundi"}, + "minute": {"past": "üks minut", "future": "ühe minuti"}, + "minutes": {"past": "{0} minutit", "future": "{0} minuti"}, + "hour": {"past": "tund aega", "future": "tunni aja"}, + "hours": {"past": "{0} tundi", "future": "{0} tunni"}, + "day": {"past": "üks päev", "future": "ühe päeva"}, + "days": {"past": "{0} päeva", "future": "{0} päeva"}, + "month": {"past": "üks kuu", "future": "ühe kuu"}, + "months": {"past": "{0} kuud", "future": "{0} kuu"}, + "year": {"past": "üks aasta", "future": "ühe aasta"}, + "years": {"past": "{0} aastat", "future": "{0} aasta"}, + } + + month_names = [ + "", + "Jaanuar", + "Veebruar", + "Märts", + "Aprill", + "Mai", + "Juuni", + "Juuli", + "August", + "September", + "Oktoober", + "November", + "Detsember", + ] + month_abbreviations = [ + "", + "Jan", + "Veb", + "Mär", + "Apr", + "Mai", + "Jun", + "Jul", + "Aug", + "Sep", + "Okt", + "Nov", + "Dets", + ] + + day_names = [ + "", + "Esmaspäev", + "Teisipäev", + "Kolmapäev", + "Neljapäev", + "Reede", + "Laupäev", + "Pühapäev", + ] + day_abbreviations = ["", "Esm", "Teis", "Kolm", "Nelj", "Re", "Lau", "Püh"] + + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + form = self.timeframes[timeframe] + if delta > 0: + _form = form["future"] + else: + _form = form["past"] + return _form.format(abs(delta)) + + +class LatvianLocale(Locale): + + names = ["lv", "lv-lv"] + + past = "pirms {0}" + future = "pēc {0}" + and_word = "un" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "tagad", + "second": "sekundes", + "seconds": "{0} sekundēm", + "minute": "minūtes", + "minutes": "{0} minūtēm", + "hour": "stundas", + "hours": "{0} stundām", + "day": "dienas", + "days": "{0} dienām", + "week": "nedēļas", + "weeks": "{0} nedēļām", + "month": "mēneša", + "months": "{0} mēnešiem", + "year": "gada", + "years": "{0} gadiem", + } + + month_names = [ + "", + "janvāris", + "februāris", + "marts", + "aprīlis", + "maijs", + "jūnijs", + "jūlijs", + "augusts", + "septembris", + "oktobris", + "novembris", + "decembris", + ] + + month_abbreviations = [ + "", + "jan", + "feb", + "marts", + "apr", + "maijs", + "jūnijs", + "jūlijs", + "aug", + "sept", + "okt", + "nov", + "dec", + ] + + day_names = [ + "", + "pirmdiena", + "otrdiena", + "trešdiena", + "ceturtdiena", + "piektdiena", + "sestdiena", + "svētdiena", ] day_abbreviations = [ - '', 'Pon', 'Tor', 'Sre', 'Čet', 'Pet', 'Sob', 'Ned' + "", + "pi", + "ot", + "tr", + "ce", + "pi", + "se", + "sv", ] -_locales = _map_locales() +class SwahiliLocale(Locale): + + names = [ + "sw", + "sw-ke", + "sw-tz", + ] + + past = "{0} iliyopita" + future = "muda wa {0}" + and_word = "na" + + timeframes = { + "now": "sasa hivi", + "second": "sekunde", + "seconds": "sekunde {0}", + "minute": "dakika moja", + "minutes": "dakika {0}", + "hour": "saa moja", + "hours": "saa {0}", + "day": "siku moja", + "days": "siku {0}", + "week": "wiki moja", + "weeks": "wiki {0}", + "month": "mwezi moja", + "months": "miezi {0}", + "year": "mwaka moja", + "years": "miaka {0}", + } + + meridians = {"am": "asu", "pm": "mch", "AM": "ASU", "PM": "MCH"} + + month_names = [ + "", + "Januari", + "Februari", + "Machi", + "Aprili", + "Mei", + "Juni", + "Julai", + "Agosti", + "Septemba", + "Oktoba", + "Novemba", + "Desemba", + ] + month_abbreviations = [ + "", + "Jan", + "Feb", + "Mac", + "Apr", + "Mei", + "Jun", + "Jul", + "Ago", + "Sep", + "Okt", + "Nov", + "Des", + ] + + day_names = [ + "", + "Jumatatu", + "Jumanne", + "Jumatano", + "Alhamisi", + "Ijumaa", + "Jumamosi", + "Jumapili", + ] + day_abbreviations = [ + "", + "Jumatatu", + "Jumanne", + "Jumatano", + "Alhamisi", + "Ijumaa", + "Jumamosi", + "Jumapili", + ] + + +class CroatianLocale(Locale): + + names = ["hr", "hr-hr"] + + past = "prije {0}" + future = "za {0}" + and_word = "i" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "upravo sad", + "second": "sekundu", + "seconds": {"double": "{0} sekunde", "higher": "{0} sekundi"}, + "minute": "minutu", + "minutes": {"double": "{0} minute", "higher": "{0} minuta"}, + "hour": "sat", + "hours": {"double": "{0} sata", "higher": "{0} sati"}, + "day": "jedan dan", + "days": {"double": "{0} dana", "higher": "{0} dana"}, + "week": "tjedan", + "weeks": {"double": "{0} tjedna", "higher": "{0} tjedana"}, + "month": "mjesec", + "months": {"double": "{0} mjeseca", "higher": "{0} mjeseci"}, + "year": "godinu", + "years": {"double": "{0} godine", "higher": "{0} godina"}, + } + + month_names = [ + "", + "siječanj", + "veljača", + "ožujak", + "travanj", + "svibanj", + "lipanj", + "srpanj", + "kolovoz", + "rujan", + "listopad", + "studeni", + "prosinac", + ] + + month_abbreviations = [ + "", + "siječ", + "velj", + "ožuj", + "trav", + "svib", + "lip", + "srp", + "kol", + "ruj", + "list", + "stud", + "pros", + ] + + day_names = [ + "", + "ponedjeljak", + "utorak", + "srijeda", + "četvrtak", + "petak", + "subota", + "nedjelja", + ] + + day_abbreviations = [ + "", + "po", + "ut", + "sr", + "če", + "pe", + "su", + "ne", + ] + + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + form = self.timeframes[timeframe] + delta = abs(delta) + if isinstance(form, Mapping): + if 1 < delta <= 4: + form = form["double"] + else: + form = form["higher"] + + return form.format(delta) + + +class LatinLocale(Locale): + + names = ["la", "la-va"] + + past = "ante {0}" + future = "in {0}" + and_word = "et" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "nunc", + "second": "secundum", + "seconds": "{0} secundis", + "minute": "minutam", + "minutes": "{0} minutis", + "hour": "horam", + "hours": "{0} horas", + "day": "diem", + "days": "{0} dies", + "week": "hebdomadem", + "weeks": "{0} hebdomades", + "month": "mensem", + "months": "{0} mensis", + "year": "annum", + "years": "{0} annos", + } + + month_names = [ + "", + "Ianuarius", + "Februarius", + "Martius", + "Aprilis", + "Maius", + "Iunius", + "Iulius", + "Augustus", + "September", + "October", + "November", + "December", + ] + + month_abbreviations = [ + "", + "Ian", + "Febr", + "Mart", + "Apr", + "Mai", + "Iun", + "Iul", + "Aug", + "Sept", + "Oct", + "Nov", + "Dec", + ] + + day_names = [ + "", + "dies Lunae", + "dies Martis", + "dies Mercurii", + "dies Iovis", + "dies Veneris", + "dies Saturni", + "dies Solis", + ] + + day_abbreviations = [ + "", + "dies Lunae", + "dies Martis", + "dies Mercurii", + "dies Iovis", + "dies Veneris", + "dies Saturni", + "dies Solis", + ] + + +class LithuanianLocale(Locale): + + names = ["lt", "lt-lt"] + + past = "prieš {0}" + future = "po {0}" + and_word = "ir" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "dabar", + "second": "sekundės", + "seconds": "{0} sekundžių", + "minute": "minutės", + "minutes": "{0} minučių", + "hour": "valandos", + "hours": "{0} valandų", + "day": "dieną", + "days": "{0} dienų", + "week": "savaitės", + "weeks": "{0} savaičių", + "month": "mėnesio", + "months": "{0} mėnesių", + "year": "metų", + "years": "{0} metų", + } + + month_names = [ + "", + "sausis", + "vasaris", + "kovas", + "balandis", + "gegužė", + "birželis", + "liepa", + "rugpjūtis", + "rugsėjis", + "spalis", + "lapkritis", + "gruodis", + ] + + month_abbreviations = [ + "", + "saus", + "vas", + "kovas", + "bal", + "geg", + "birž", + "liepa", + "rugp", + "rugs", + "spalis", + "lapkr", + "gr", + ] + + day_names = [ + "", + "pirmadienis", + "antradienis", + "trečiadienis", + "ketvirtadienis", + "penktadienis", + "šeštadienis", + "sekmadienis", + ] + + day_abbreviations = [ + "", + "pi", + "an", + "tr", + "ke", + "pe", + "še", + "se", + ] + + +class MalayLocale(Locale): + + names = ["ms", "ms-my", "ms-bn"] + + past = "{0} yang lalu" + future = "dalam {0}" + and_word = "dan" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "sekarang", + "second": "saat", + "seconds": "{0} saat", + "minute": "minit", + "minutes": "{0} minit", + "hour": "jam", + "hours": "{0} jam", + "day": "hari", + "days": "{0} hari", + "week": "minggu", + "weeks": "{0} minggu", + "month": "bulan", + "months": "{0} bulan", + "year": "tahun", + "years": "{0} tahun", + } + + month_names = [ + "", + "Januari", + "Februari", + "Mac", + "April", + "Mei", + "Jun", + "Julai", + "Ogos", + "September", + "Oktober", + "November", + "Disember", + ] + + month_abbreviations = [ + "", + "Jan.", + "Feb.", + "Mac", + "Apr.", + "Mei", + "Jun", + "Julai", + "Og.", + "Sept.", + "Okt.", + "Nov.", + "Dis.", + ] + + day_names = [ + "", + "Isnin", + "Selasa", + "Rabu", + "Khamis", + "Jumaat", + "Sabtu", + "Ahad", + ] + + day_abbreviations = [ + "", + "Isnin", + "Selasa", + "Rabu", + "Khamis", + "Jumaat", + "Sabtu", + "Ahad", + ] + + +class MalteseLocale(Locale): + + names = ["mt", "mt-mt"] + + past = "{0} ilu" + future = "fi {0}" + and_word = "u" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "issa", + "second": "sekonda", + "seconds": "{0} sekondi", + "minute": "minuta", + "minutes": "{0} minuti", + "hour": "siegħa", + "hours": {"dual": "{0} sagħtejn", "plural": "{0} sigħat"}, + "day": "jum", + "days": {"dual": "{0} jumejn", "plural": "{0} ijiem"}, + "week": "ġimgħa", + "weeks": {"dual": "{0} ġimagħtejn", "plural": "{0} ġimgħat"}, + "month": "xahar", + "months": {"dual": "{0} xahrejn", "plural": "{0} xhur"}, + "year": "sena", + "years": {"dual": "{0} sentejn", "plural": "{0} snin"}, + } + + month_names = [ + "", + "Jannar", + "Frar", + "Marzu", + "April", + "Mejju", + "Ġunju", + "Lulju", + "Awwissu", + "Settembru", + "Ottubru", + "Novembru", + "Diċembru", + ] + + month_abbreviations = [ + "", + "Jan", + "Fr", + "Mar", + "Apr", + "Mejju", + "Ġun", + "Lul", + "Aw", + "Sett", + "Ott", + "Nov", + "Diċ", + ] + + day_names = [ + "", + "It-Tnejn", + "It-Tlieta", + "L-Erbgħa", + "Il-Ħamis", + "Il-Ġimgħa", + "Is-Sibt", + "Il-Ħadd", + ] + + day_abbreviations = [ + "", + "T", + "TL", + "E", + "Ħ", + "Ġ", + "S", + "Ħ", + ] + + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + form = self.timeframes[timeframe] + delta = abs(delta) + if isinstance(form, Mapping): + if delta == 2: + form = form["dual"] + else: + form = form["plural"] + + return form.format(delta) + + +class SamiLocale(Locale): + + names = ["se", "se-fi", "se-no", "se-se"] + + past = "{0} dassái" + future = "{0} " # NOTE: couldn't find preposition for Sami here, none needed? + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "dál", + "second": "sekunda", + "seconds": "{0} sekundda", + "minute": "minuhta", + "minutes": "{0} minuhta", + "hour": "diimmu", + "hours": "{0} diimmu", + "day": "beaivvi", + "days": "{0} beaivvi", + "week": "vahku", + "weeks": "{0} vahku", + "month": "mánu", + "months": "{0} mánu", + "year": "jagi", + "years": "{0} jagi", + } + + month_names = [ + "", + "Ođđajagimánnu", + "Guovvamánnu", + "Njukčamánnu", + "Cuoŋománnu", + "Miessemánnu", + "Geassemánnu", + "Suoidnemánnu", + "Borgemánnu", + "Čakčamánnu", + "Golggotmánnu", + "Skábmamánnu", + "Juovlamánnu", + ] + + month_abbreviations = [ + "", + "Ođđajagimánnu", + "Guovvamánnu", + "Njukčamánnu", + "Cuoŋománnu", + "Miessemánnu", + "Geassemánnu", + "Suoidnemánnu", + "Borgemánnu", + "Čakčamánnu", + "Golggotmánnu", + "Skábmamánnu", + "Juovlamánnu", + ] + + day_names = [ + "", + "Mánnodat", + "Disdat", + "Gaskavahkku", + "Duorastat", + "Bearjadat", + "Lávvordat", + "Sotnabeaivi", + ] + + day_abbreviations = [ + "", + "Mánnodat", + "Disdat", + "Gaskavahkku", + "Duorastat", + "Bearjadat", + "Lávvordat", + "Sotnabeaivi", + ] + + +class OdiaLocale(Locale): + + names = ["or", "or-in"] + + past = "{0} ପୂର୍ବେ" + future = "{0} ପରେ" + + timeframes = { + "now": "ବର୍ତ୍ତମାନ", + "second": "ଏକ ସେକେଣ୍ଡ", + "seconds": "{0} ସେକେଣ୍ଡ", + "minute": "ଏକ ମିନଟ", + "minutes": "{0} ମିନଟ", + "hour": "ଏକ ଘଣ୍ଟା", + "hours": "{0} ଘଣ୍ଟା", + "day": "ଏକ ଦିନ", + "days": "{0} ଦିନ", + "month": "ଏକ ମାସ", + "months": "{0} ମାସ ", + "year": "ଏକ ବର୍ଷ", + "years": "{0} ବର୍ଷ", + } + + meridians = {"am": "ପୂର୍ବାହ୍ନ", "pm": "ଅପରାହ୍ନ", "AM": "ପୂର୍ବାହ୍ନ", "PM": "ଅପରାହ୍ନ"} + + month_names = [ + "", + "ଜାନୁଆରୀ", + "ଫେବୃଆରୀ", + "ମାର୍ଚ୍ଚ୍", + "ଅପ୍ରେଲ", + "ମଇ", + "ଜୁନ୍", + "ଜୁଲାଇ", + "ଅଗଷ୍ଟ", + "ସେପ୍ଟେମ୍ବର", + "ଅକ୍ଟୋବର୍", + "ନଭେମ୍ବର୍", + "ଡିସେମ୍ବର୍", + ] + month_abbreviations = [ + "", + "ଜାନୁ", + "ଫେବୃ", + "ମାର୍ଚ୍ଚ୍", + "ଅପ୍ରେ", + "ମଇ", + "ଜୁନ୍", + "ଜୁଲା", + "ଅଗ", + "ସେପ୍ଟେ", + "ଅକ୍ଟୋ", + "ନଭେ", + "ଡିସେ", + ] + + day_names = [ + "", + "ସୋମବାର", + "ମଙ୍ଗଳବାର", + "ବୁଧବାର", + "ଗୁରୁବାର", + "ଶୁକ୍ରବାର", + "ଶନିବାର", + "ରବିବାର", + ] + day_abbreviations = [ + "", + "ସୋମ", + "ମଙ୍ଗଳ", + "ବୁଧ", + "ଗୁରୁ", + "ଶୁକ୍ର", + "ଶନି", + "ରବି", + ] + + def _ordinal_number(self, n: int) -> str: + if n > 10 or n == 0: + return f"{n}ତମ" + if n in [1, 5, 7, 8, 9, 10]: + return f"{n}ମ" + if n in [2, 3]: + return f"{n}ୟ" + if n == 4: + return f"{n}ର୍ଥ" + if n == 6: + return f"{n}ଷ୍ଠ" + return "" + + +class SerbianLocale(Locale): + + names = ["sr", "sr-rs", "sr-sp"] + + past = "pre {0}" + future = "za {0}" + and_word = "i" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "sada", + "second": "sekundu", + "seconds": {"double": "{0} sekunde", "higher": "{0} sekundi"}, + "minute": "minutu", + "minutes": {"double": "{0} minute", "higher": "{0} minuta"}, + "hour": "sat", + "hours": {"double": "{0} sata", "higher": "{0} sati"}, + "day": "dan", + "days": {"double": "{0} dana", "higher": "{0} dana"}, + "week": "nedelju", + "weeks": {"double": "{0} nedelje", "higher": "{0} nedelja"}, + "month": "mesec", + "months": {"double": "{0} meseca", "higher": "{0} meseci"}, + "year": "godinu", + "years": {"double": "{0} godine", "higher": "{0} godina"}, + } + + month_names = [ + "", + "januar", # Јануар + "februar", # фебруар + "mart", # март + "april", # април + "maj", # мај + "juni", # јун + "juli", # јул + "avgust", # август + "septembar", # септембар + "oktobar", # октобар + "novembar", # новембар + "decembar", # децембар + ] + + month_abbreviations = [ + "", + "jan.", + "febr.", + "mart", + "april", + "maj", + "juni", + "juli", + "avg.", + "sept.", + "okt.", + "nov.", + "dec.", + ] + + day_names = [ + "", + "ponedeljak", # понедељак + "utorak", # уторак + "sreda", # среда + "četvrtak", # четвртак + "petak", # петак + "subota", # субота + "nedelja", # недеља + ] + + day_abbreviations = [ + "", + "po", # по + "ut", # ут + "sr", # ср + "če", # че + "pe", # пе + "su", # су + "ne", # не + ] + + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + form = self.timeframes[timeframe] + delta = abs(delta) + if isinstance(form, Mapping): + if 1 < delta <= 4: + form = form["double"] + else: + form = form["higher"] + + return form.format(delta) + + +class LuxembourgishLocale(Locale): + + names = ["lb", "lb-lu"] + + past = "virun {0}" + future = "an {0}" + and_word = "an" + + timeframes = { + "now": "just elo", + "second": "enger Sekonn", + "seconds": "{0} Sekonnen", + "minute": "enger Minutt", + "minutes": "{0} Minutten", + "hour": "enger Stonn", + "hours": "{0} Stonnen", + "day": "engem Dag", + "days": "{0} Deeg", + "week": "enger Woch", + "weeks": "{0} Wochen", + "month": "engem Mount", + "months": "{0} Méint", + "year": "engem Joer", + "years": "{0} Jahren", + } + + timeframes_only_distance = timeframes.copy() + timeframes_only_distance["second"] = "eng Sekonn" + timeframes_only_distance["minute"] = "eng Minutt" + timeframes_only_distance["hour"] = "eng Stonn" + timeframes_only_distance["day"] = "een Dag" + timeframes_only_distance["days"] = "{0} Deeg" + timeframes_only_distance["week"] = "eng Woch" + timeframes_only_distance["month"] = "ee Mount" + timeframes_only_distance["months"] = "{0} Méint" + timeframes_only_distance["year"] = "ee Joer" + timeframes_only_distance["years"] = "{0} Joer" + + month_names = [ + "", + "Januar", + "Februar", + "Mäerz", + "Abrëll", + "Mee", + "Juni", + "Juli", + "August", + "September", + "Oktouber", + "November", + "Dezember", + ] + + month_abbreviations = [ + "", + "Jan", + "Feb", + "Mäe", + "Abr", + "Mee", + "Jun", + "Jul", + "Aug", + "Sep", + "Okt", + "Nov", + "Dez", + ] + + day_names = [ + "", + "Méindeg", + "Dënschdeg", + "Mëttwoch", + "Donneschdeg", + "Freideg", + "Samschdeg", + "Sonndeg", + ] + + day_abbreviations = ["", "Méi", "Dën", "Mët", "Don", "Fre", "Sam", "Son"] + + def _ordinal_number(self, n: int) -> str: + return f"{n}." + + def describe( + self, + timeframe: TimeFrameLiteral, + delta: Union[int, float] = 0, + only_distance: bool = False, + ) -> str: + + if not only_distance: + return super().describe(timeframe, delta, only_distance) + + # Luxembourgish uses a different case without 'in' or 'ago' + humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) + + return humanized + + +class ZuluLocale(Locale): + + names = ["zu", "zu-za"] + + past = "{0} edlule" + future = "{0} " + and_word = "futhi" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[Mapping[str, str], str]]] = { + "now": "manje", + "second": {"past": "umzuzwana", "future": "ngomzuzwana"}, + "seconds": {"past": "{0} imizuzwana", "future": "{0} ngemizuzwana"}, + "minute": {"past": "umzuzu", "future": "ngomzuzu"}, + "minutes": {"past": "{0} imizuzu", "future": "{0} ngemizuzu"}, + "hour": {"past": "ihora", "future": "ngehora"}, + "hours": {"past": "{0} amahora", "future": "{0} emahoreni"}, + "day": {"past": "usuku", "future": "ngosuku"}, + "days": {"past": "{0} izinsuku", "future": "{0} ezinsukwini"}, + "week": {"past": "isonto", "future": "ngesonto"}, + "weeks": {"past": "{0} amasonto", "future": "{0} emasontweni"}, + "month": {"past": "inyanga", "future": "ngenyanga"}, + "months": {"past": "{0} izinyanga", "future": "{0} ezinyangeni"}, + "year": {"past": "unyaka", "future": "ngonyak"}, + "years": {"past": "{0} iminyaka", "future": "{0} eminyakeni"}, + } + + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + """Zulu aware time frame format function, takes into account + the differences between past and future forms.""" + abs_delta = abs(delta) + form = self.timeframes[timeframe] + + if isinstance(form, str): + return form.format(abs_delta) + + if delta > 0: + key = "future" + else: + key = "past" + form = form[key] + + return form.format(abs_delta) + + month_names = [ + "", + "uMasingane", + "uNhlolanja", + "uNdasa", + "UMbasa", + "UNhlaba", + "UNhlangulana", + "uNtulikazi", + "UNcwaba", + "uMandulo", + "uMfumfu", + "uLwezi", + "uZibandlela", + ] + + month_abbreviations = [ + "", + "uMasingane", + "uNhlolanja", + "uNdasa", + "UMbasa", + "UNhlaba", + "UNhlangulana", + "uNtulikazi", + "UNcwaba", + "uMandulo", + "uMfumfu", + "uLwezi", + "uZibandlela", + ] + + day_names = [ + "", + "uMsombuluko", + "uLwesibili", + "uLwesithathu", + "uLwesine", + "uLwesihlanu", + "uMgqibelo", + "iSonto", + ] + + day_abbreviations = [ + "", + "uMsombuluko", + "uLwesibili", + "uLwesithathu", + "uLwesine", + "uLwesihlanu", + "uMgqibelo", + "iSonto", + ] + + +class TamilLocale(Locale): + + names = ["ta", "ta-in", "ta-lk"] + + past = "{0} நேரத்திற்கு முன்பு" + future = "இல் {0}" + + timeframes = { + "now": "இப்போது", + "second": "ஒரு இரண்டாவது", + "seconds": "{0} விநாடிகள்", + "minute": "ஒரு நிமிடம்", + "minutes": "{0} நிமிடங்கள்", + "hour": "ஒரு மணி", + "hours": "{0} மணிநேரம்", + "day": "ஒரு நாள்", + "days": "{0} நாட்கள்", + "week": "ஒரு வாரம்", + "weeks": "{0} வாரங்கள்", + "month": "ஒரு மாதம்", + "months": "{0} மாதங்கள்", + "year": "ஒரு ஆண்டு", + "years": "{0} ஆண்டுகள்", + } + + month_names = [ + "", + "சித்திரை", + "வைகாசி", + "ஆனி", + "ஆடி", + "ஆவணி", + "புரட்டாசி", + "ஐப்பசி", + "கார்த்திகை", + "மார்கழி", + "தை", + "மாசி", + "பங்குனி", + ] + + month_abbreviations = [ + "", + "ஜன", + "பிப்", + "மார்", + "ஏப்", + "மே", + "ஜூன்", + "ஜூலை", + "ஆக", + "செப்", + "அக்", + "நவ", + "டிச", + ] + + day_names = [ + "", + "திங்கட்கிழமை", + "செவ்வாய்க்கிழமை", + "புதன்கிழமை", + "வியாழக்கிழமை", + "வெள்ளிக்கிழமை", + "சனிக்கிழமை", + "ஞாயிற்றுக்கிழமை", + ] + + day_abbreviations = [ + "", + "திங்கட்", + "செவ்வாய்", + "புதன்", + "வியாழன்", + "வெள்ளி", + "சனி", + "ஞாயிறு", + ] + + def _ordinal_number(self, n: int) -> str: + if n == 1: + return f"{n}வது" + elif n >= 0: + return f"{n}ஆம்" + else: + return "" + + +class AlbanianLocale(Locale): + + names = ["sq", "sq-al"] + + past = "{0} më parë" + future = "në {0}" + and_word = "dhe" + + timeframes = { + "now": "tani", + "second": "sekondë", + "seconds": "{0} sekonda", + "minute": "minutë", + "minutes": "{0} minuta", + "hour": "orë", + "hours": "{0} orë", + "day": "ditë", + "days": "{0} ditë", + "week": "javë", + "weeks": "{0} javë", + "month": "muaj", + "months": "{0} muaj", + "year": "vit", + "years": "{0} vjet", + } + + month_names = [ + "", + "janar", + "shkurt", + "mars", + "prill", + "maj", + "qershor", + "korrik", + "gusht", + "shtator", + "tetor", + "nëntor", + "dhjetor", + ] + + month_abbreviations = [ + "", + "jan", + "shk", + "mar", + "pri", + "maj", + "qer", + "korr", + "gush", + "sht", + "tet", + "nën", + "dhj", + ] + + day_names = [ + "", + "e hënë", + "e martë", + "e mërkurë", + "e enjte", + "e premte", + "e shtunë", + "e diel", + ] + + day_abbreviations = [ + "", + "hën", + "mar", + "mër", + "enj", + "pre", + "sht", + "die", + ] diff --git a/lib/arrow/parser.py b/lib/arrow/parser.py index f3ed56cf..e95d78b0 100644 --- a/lib/arrow/parser.py +++ b/lib/arrow/parser.py @@ -1,205 +1,555 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals +"""Provides the :class:`Arrow ` class, a better way to parse datetime strings.""" -from datetime import datetime -from dateutil import tz import re +import sys +from datetime import datetime, timedelta +from datetime import tzinfo as dt_tzinfo +from functools import lru_cache +from typing import ( + Any, + ClassVar, + Dict, + Iterable, + List, + Match, + Optional, + Pattern, + SupportsFloat, + SupportsInt, + Tuple, + Union, + cast, + overload, +) + +from dateutil import tz + from arrow import locales +from arrow.constants import DEFAULT_LOCALE +from arrow.util import next_weekday, normalize_timestamp + +if sys.version_info < (3, 8): # pragma: no cover + from typing_extensions import Literal, TypedDict +else: + from typing import Literal, TypedDict # pragma: no cover -class ParserError(RuntimeError): +class ParserError(ValueError): pass -class DateTimeParser(object): - - _FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|S+|ZZ?Z?|a|A|X)') - _ESCAPE_RE = re.compile('\[[^\[\]]*\]') - - _ONE_OR_MORE_DIGIT_RE = re.compile('\d+') - _ONE_OR_TWO_DIGIT_RE = re.compile('\d{1,2}') - _FOUR_DIGIT_RE = re.compile('\d{4}') - _TWO_DIGIT_RE = re.compile('\d{2}') - _TZ_RE = re.compile('[+\-]?\d{2}:?(\d{2})?') - _TZ_NAME_RE = re.compile('\w[\w+\-/]+') +# Allows for ParserErrors to be propagated from _build_datetime() +# when day_of_year errors occur. +# Before this, the ParserErrors were caught by the try/except in +# _parse_multiformat() and the appropriate error message was not +# transmitted to the user. +class ParserMatchError(ParserError): + pass - _BASE_INPUT_RE_MAP = { - 'YYYY': _FOUR_DIGIT_RE, - 'YY': _TWO_DIGIT_RE, - 'MM': _TWO_DIGIT_RE, - 'M': _ONE_OR_TWO_DIGIT_RE, - 'DD': _TWO_DIGIT_RE, - 'D': _ONE_OR_TWO_DIGIT_RE, - 'HH': _TWO_DIGIT_RE, - 'H': _ONE_OR_TWO_DIGIT_RE, - 'hh': _TWO_DIGIT_RE, - 'h': _ONE_OR_TWO_DIGIT_RE, - 'mm': _TWO_DIGIT_RE, - 'm': _ONE_OR_TWO_DIGIT_RE, - 'ss': _TWO_DIGIT_RE, - 's': _ONE_OR_TWO_DIGIT_RE, - 'X': re.compile('\d+'), - 'ZZZ': _TZ_NAME_RE, - 'ZZ': _TZ_RE, - 'Z': _TZ_RE, - 'S': _ONE_OR_MORE_DIGIT_RE, +_WEEKDATE_ELEMENT = Union[str, bytes, SupportsInt, bytearray] + +_FORMAT_TYPE = Literal[ + "YYYY", + "YY", + "MM", + "M", + "DDDD", + "DDD", + "DD", + "D", + "HH", + "H", + "hh", + "h", + "mm", + "m", + "ss", + "s", + "X", + "x", + "ZZZ", + "ZZ", + "Z", + "S", + "W", + "MMMM", + "MMM", + "Do", + "dddd", + "ddd", + "d", + "a", + "A", +] + + +class _Parts(TypedDict, total=False): + year: int + month: int + day_of_year: int + day: int + hour: int + minute: int + second: int + microsecond: int + timestamp: float + expanded_timestamp: int + tzinfo: dt_tzinfo + am_pm: Literal["am", "pm"] + day_of_week: int + weekdate: Tuple[_WEEKDATE_ELEMENT, _WEEKDATE_ELEMENT, Optional[_WEEKDATE_ELEMENT]] + + +class DateTimeParser: + _FORMAT_RE: ClassVar[Pattern[str]] = re.compile( + r"(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|S+|ZZ?Z?|a|A|x|X|W)" + ) + _ESCAPE_RE: ClassVar[Pattern[str]] = re.compile(r"\[[^\[\]]*\]") + + _ONE_OR_TWO_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{1,2}") + _ONE_OR_TWO_OR_THREE_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{1,3}") + _ONE_OR_MORE_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d+") + _TWO_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{2}") + _THREE_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{3}") + _FOUR_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{4}") + _TZ_Z_RE: ClassVar[Pattern[str]] = re.compile(r"([\+\-])(\d{2})(?:(\d{2}))?|Z") + _TZ_ZZ_RE: ClassVar[Pattern[str]] = re.compile(r"([\+\-])(\d{2})(?:\:(\d{2}))?|Z") + _TZ_NAME_RE: ClassVar[Pattern[str]] = re.compile(r"\w[\w+\-/]+") + # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will + # break cases like "15 Jul 2000" and a format list (see issue #447) + _TIMESTAMP_RE: ClassVar[Pattern[str]] = re.compile(r"^\-?\d+\.?\d+$") + _TIMESTAMP_EXPANDED_RE: ClassVar[Pattern[str]] = re.compile(r"^\-?\d+$") + _TIME_RE: ClassVar[Pattern[str]] = re.compile( + r"^(\d{2})(?:\:?(\d{2}))?(?:\:?(\d{2}))?(?:([\.\,])(\d+))?$" + ) + _WEEK_DATE_RE: ClassVar[Pattern[str]] = re.compile( + r"(?P\d{4})[\-]?W(?P\d{2})[\-]?(?P\d)?" + ) + + _BASE_INPUT_RE_MAP: ClassVar[Dict[_FORMAT_TYPE, Pattern[str]]] = { + "YYYY": _FOUR_DIGIT_RE, + "YY": _TWO_DIGIT_RE, + "MM": _TWO_DIGIT_RE, + "M": _ONE_OR_TWO_DIGIT_RE, + "DDDD": _THREE_DIGIT_RE, + "DDD": _ONE_OR_TWO_OR_THREE_DIGIT_RE, + "DD": _TWO_DIGIT_RE, + "D": _ONE_OR_TWO_DIGIT_RE, + "HH": _TWO_DIGIT_RE, + "H": _ONE_OR_TWO_DIGIT_RE, + "hh": _TWO_DIGIT_RE, + "h": _ONE_OR_TWO_DIGIT_RE, + "mm": _TWO_DIGIT_RE, + "m": _ONE_OR_TWO_DIGIT_RE, + "ss": _TWO_DIGIT_RE, + "s": _ONE_OR_TWO_DIGIT_RE, + "X": _TIMESTAMP_RE, + "x": _TIMESTAMP_EXPANDED_RE, + "ZZZ": _TZ_NAME_RE, + "ZZ": _TZ_ZZ_RE, + "Z": _TZ_Z_RE, + "S": _ONE_OR_MORE_DIGIT_RE, + "W": _WEEK_DATE_RE, } - MARKERS = ['YYYY', 'MM', 'DD'] - SEPARATORS = ['-', '/', '.'] + SEPARATORS: ClassVar[List[str]] = ["-", "/", "."] - def __init__(self, locale='en_us'): + locale: locales.Locale + _input_re_map: Dict[_FORMAT_TYPE, Pattern[str]] + + def __init__(self, locale: str = DEFAULT_LOCALE, cache_size: int = 0) -> None: self.locale = locales.get_locale(locale) self._input_re_map = self._BASE_INPUT_RE_MAP.copy() - self._input_re_map.update({ - 'MMMM': self._choice_re(self.locale.month_names[1:], re.IGNORECASE), - 'MMM': self._choice_re(self.locale.month_abbreviations[1:], - re.IGNORECASE), - 'Do': re.compile(self.locale.ordinal_day_re), - 'dddd': self._choice_re(self.locale.day_names[1:], re.IGNORECASE), - 'ddd': self._choice_re(self.locale.day_abbreviations[1:], - re.IGNORECASE), - 'd' : re.compile("[1-7]"), - 'a': self._choice_re( - (self.locale.meridians['am'], self.locale.meridians['pm']) - ), - # note: 'A' token accepts both 'am/pm' and 'AM/PM' formats to - # ensure backwards compatibility of this token - 'A': self._choice_re(self.locale.meridians.values()) - }) + self._input_re_map.update( + { + "MMMM": self._generate_choice_re( + self.locale.month_names[1:], re.IGNORECASE + ), + "MMM": self._generate_choice_re( + self.locale.month_abbreviations[1:], re.IGNORECASE + ), + "Do": re.compile(self.locale.ordinal_day_re), + "dddd": self._generate_choice_re( + self.locale.day_names[1:], re.IGNORECASE + ), + "ddd": self._generate_choice_re( + self.locale.day_abbreviations[1:], re.IGNORECASE + ), + "d": re.compile(r"[1-7]"), + "a": self._generate_choice_re( + (self.locale.meridians["am"], self.locale.meridians["pm"]) + ), + # note: 'A' token accepts both 'am/pm' and 'AM/PM' formats to + # ensure backwards compatibility of this token + "A": self._generate_choice_re(self.locale.meridians.values()), + } + ) + if cache_size > 0: + self._generate_pattern_re = lru_cache(maxsize=cache_size)( # type: ignore + self._generate_pattern_re + ) - def parse_iso(self, string): + # TODO: since we support more than ISO 8601, we should rename this function + # IDEA: break into multiple functions + def parse_iso( + self, datetime_string: str, normalize_whitespace: bool = False + ) -> datetime: - has_time = 'T' in string or ' ' in string.strip() - space_divider = ' ' in string.strip() + if normalize_whitespace: + datetime_string = re.sub(r"\s+", " ", datetime_string.strip()) + + has_space_divider = " " in datetime_string + has_t_divider = "T" in datetime_string + + num_spaces = datetime_string.count(" ") + if has_space_divider and num_spaces != 1 or has_t_divider and num_spaces > 0: + raise ParserError( + f"Expected an ISO 8601-like string, but was given {datetime_string!r}. " + "Try passing in a format string to resolve this." + ) + + has_time = has_space_divider or has_t_divider + has_tz = False + + # date formats (ISO 8601 and others) to test against + # NOTE: YYYYMM is omitted to avoid confusion with YYMMDD (no longer part of ISO 8601, but is still often used) + formats = [ + "YYYY-MM-DD", + "YYYY-M-DD", + "YYYY-M-D", + "YYYY/MM/DD", + "YYYY/M/DD", + "YYYY/M/D", + "YYYY.MM.DD", + "YYYY.M.DD", + "YYYY.M.D", + "YYYYMMDD", + "YYYY-DDDD", + "YYYYDDDD", + "YYYY-MM", + "YYYY/MM", + "YYYY.MM", + "YYYY", + "W", + ] if has_time: - if space_divider: - date_string, time_string = string.split(' ', 1) + + if has_space_divider: + date_string, time_string = datetime_string.split(" ", 1) else: - date_string, time_string = string.split('T', 1) - time_parts = re.split('[+-]', time_string, 1) - has_tz = len(time_parts) > 1 - has_seconds = time_parts[0].count(':') > 1 - has_subseconds = re.search('[.,]', time_parts[0]) + date_string, time_string = datetime_string.split("T", 1) + + time_parts = re.split(r"[\+\-Z]", time_string, 1, re.IGNORECASE) + + time_components: Optional[Match[str]] = self._TIME_RE.match(time_parts[0]) + + if time_components is None: + raise ParserError( + "Invalid time component provided. " + "Please specify a format or provide a valid time component in the basic or extended ISO 8601 time format." + ) + + ( + hours, + minutes, + seconds, + subseconds_sep, + subseconds, + ) = time_components.groups() + + has_tz = len(time_parts) == 2 + has_minutes = minutes is not None + has_seconds = seconds is not None + has_subseconds = subseconds is not None + + is_basic_time_format = ":" not in time_parts[0] + tz_format = "Z" + + # use 'ZZ' token instead since tz offset is present in non-basic format + if has_tz and ":" in time_parts[1]: + tz_format = "ZZ" + + time_sep = "" if is_basic_time_format else ":" if has_subseconds: - formats = ['YYYY-MM-DDTHH:mm:ss%sS' % has_subseconds.group()] + time_string = "HH{time_sep}mm{time_sep}ss{subseconds_sep}S".format( + time_sep=time_sep, subseconds_sep=subseconds_sep + ) elif has_seconds: - formats = ['YYYY-MM-DDTHH:mm:ss'] + time_string = "HH{time_sep}mm{time_sep}ss".format(time_sep=time_sep) + elif has_minutes: + time_string = f"HH{time_sep}mm" else: - formats = ['YYYY-MM-DDTHH:mm'] - else: - has_tz = False - # generate required formats: YYYY-MM-DD, YYYY-MM-DD, YYYY - # using various separators: -, /, . - l = len(self.MARKERS) - formats = [separator.join(self.MARKERS[:l-i]) - for i in range(l) - for separator in self.SEPARATORS] + time_string = "HH" + + if has_space_divider: + formats = [f"{f} {time_string}" for f in formats] + else: + formats = [f"{f}T{time_string}" for f in formats] if has_time and has_tz: - formats = [f + 'Z' for f in formats] + # Add "Z" or "ZZ" to the format strings to indicate to + # _parse_token() that a timezone needs to be parsed + formats = [f"{f}{tz_format}" for f in formats] - if space_divider: - formats = [item.replace('T', ' ', 1) for item in formats] + return self._parse_multiformat(datetime_string, formats) - return self._parse_multiformat(string, formats) + def parse( + self, + datetime_string: str, + fmt: Union[List[str], str], + normalize_whitespace: bool = False, + ) -> datetime: - def parse(self, string, fmt): + if normalize_whitespace: + datetime_string = re.sub(r"\s+", " ", datetime_string) if isinstance(fmt, list): - return self._parse_multiformat(string, fmt) + return self._parse_multiformat(datetime_string, fmt) + + try: + fmt_tokens: List[_FORMAT_TYPE] + fmt_pattern_re: Pattern[str] + fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt) + except re.error as e: + raise ParserMatchError( + f"Failed to generate regular expression pattern: {e}." + ) + + match = fmt_pattern_re.search(datetime_string) + + if match is None: + raise ParserMatchError( + f"Failed to match {fmt!r} when parsing {datetime_string!r}." + ) + + parts: _Parts = {} + for token in fmt_tokens: + value: Union[Tuple[str, str, str], str] + if token == "Do": + value = match.group("value") + elif token == "W": + value = (match.group("year"), match.group("week"), match.group("day")) + else: + value = match.group(token) + + if value is None: + raise ParserMatchError( + f"Unable to find a match group for the specified token {token!r}." + ) + + self._parse_token(token, value, parts) # type: ignore + + return self._build_datetime(parts) + + def _generate_pattern_re(self, fmt: str) -> Tuple[List[_FORMAT_TYPE], Pattern[str]]: # fmt is a string of tokens like 'YYYY-MM-DD' # we construct a new string by replacing each # token by its pattern: # 'YYYY-MM-DD' -> '(?P\d{4})-(?P\d{2})-(?P
\d{2})' - tokens = [] + tokens: List[_FORMAT_TYPE] = [] offset = 0 + # Escape all special RegEx chars + escaped_fmt = re.escape(fmt) + # Extract the bracketed expressions to be reinserted later. - escaped_fmt = re.sub(self._ESCAPE_RE, "#" , fmt) + escaped_fmt = re.sub(self._ESCAPE_RE, "#", escaped_fmt) + # Any number of S is the same as one. - escaped_fmt = re.sub('S+', 'S', escaped_fmt) + # TODO: allow users to specify the number of digits to parse + escaped_fmt = re.sub(r"S+", "S", escaped_fmt) + escaped_data = re.findall(self._ESCAPE_RE, fmt) fmt_pattern = escaped_fmt for m in self._FORMAT_RE.finditer(escaped_fmt): - token = m.group(0) + token: _FORMAT_TYPE = cast(_FORMAT_TYPE, m.group(0)) try: input_re = self._input_re_map[token] except KeyError: - raise ParserError('Unrecognized token \'{0}\''.format(token)) - input_pattern = '(?P<{0}>{1})'.format(token, input_re.pattern) + raise ParserError(f"Unrecognized token {token!r}.") + input_pattern = f"(?P<{token}>{input_re.pattern})" tokens.append(token) # a pattern doesn't have the same length as the token # it replaces! We keep the difference in the offset variable. # This works because the string is scanned left-to-right and matches # are returned in the order found by finditer. - fmt_pattern = fmt_pattern[:m.start() + offset] + input_pattern + fmt_pattern[m.end() + offset:] + fmt_pattern = ( + fmt_pattern[: m.start() + offset] + + input_pattern + + fmt_pattern[m.end() + offset :] + ) offset += len(input_pattern) - (m.end() - m.start()) final_fmt_pattern = "" - a = fmt_pattern.split("#") - b = escaped_data + split_fmt = fmt_pattern.split(r"\#") - # Due to the way Python splits, 'a' will always be longer - for i in range(len(a)): - final_fmt_pattern += a[i] - if i < len(b): - final_fmt_pattern += b[i][1:-1] + # Due to the way Python splits, 'split_fmt' will always be longer + for i in range(len(split_fmt)): + final_fmt_pattern += split_fmt[i] + if i < len(escaped_data): + final_fmt_pattern += escaped_data[i][1:-1] - match = re.search(final_fmt_pattern, string, flags=re.IGNORECASE) - if match is None: - raise ParserError('Failed to match \'{0}\' when parsing \'{1}\''.format(final_fmt_pattern, string)) - parts = {} - for token in tokens: - if token == 'Do': - value = match.group('value') - else: - value = match.group(token) - self._parse_token(token, value, parts) - return self._build_datetime(parts) + # Wrap final_fmt_pattern in a custom word boundary to strictly + # match the formatting pattern and filter out date and time formats + # that include junk such as: blah1998-09-12 blah, blah 1998-09-12blah, + # blah1998-09-12blah. The custom word boundary matches every character + # that is not a whitespace character to allow for searching for a date + # and time string in a natural language sentence. Therefore, searching + # for a string of the form YYYY-MM-DD in "blah 1998-09-12 blah" will + # work properly. + # Certain punctuation before or after the target pattern such as + # "1998-09-12," is permitted. For the full list of valid punctuation, + # see the documentation. - def _parse_token(self, token, value, parts): + starting_word_boundary = ( + r"(?\s])" # This is the list of punctuation that is ok before the + # pattern (i.e. "It can't not be these characters before the pattern") + r"(\b|^)" + # The \b is to block cases like 1201912 but allow 201912 for pattern YYYYMM. The ^ was necessary to allow a + # negative number through i.e. before epoch numbers + ) + ending_word_boundary = ( + r"(?=[\,\.\;\:\?\!\"\'\`\[\]\{\}\(\)\<\>]?" # Positive lookahead stating that these punctuation marks + # can appear after the pattern at most 1 time + r"(?!\S))" # Don't allow any non-whitespace character after the punctuation + ) + bounded_fmt_pattern = r"{}{}{}".format( + starting_word_boundary, final_fmt_pattern, ending_word_boundary + ) - if token == 'YYYY': - parts['year'] = int(value) - elif token == 'YY': + return tokens, re.compile(bounded_fmt_pattern, flags=re.IGNORECASE) + + @overload + def _parse_token( + self, + token: Literal[ + "YYYY", + "YY", + "MM", + "M", + "DDDD", + "DDD", + "DD", + "D", + "Do", + "HH", + "hh", + "h", + "H", + "mm", + "m", + "ss", + "s", + "x", + ], + value: Union[str, bytes, SupportsInt, bytearray], + parts: _Parts, + ) -> None: + ... # pragma: no cover + + @overload + def _parse_token( + self, + token: Literal["X"], + value: Union[str, bytes, SupportsFloat, bytearray], + parts: _Parts, + ) -> None: + ... # pragma: no cover + + @overload + def _parse_token( + self, + token: Literal["MMMM", "MMM", "dddd", "ddd", "S"], + value: Union[str, bytes, bytearray], + parts: _Parts, + ) -> None: + ... # pragma: no cover + + @overload + def _parse_token( + self, + token: Literal["a", "A", "ZZZ", "ZZ", "Z"], + value: Union[str, bytes], + parts: _Parts, + ) -> None: + ... # pragma: no cover + + @overload + def _parse_token( + self, + token: Literal["W"], + value: Tuple[_WEEKDATE_ELEMENT, _WEEKDATE_ELEMENT, Optional[_WEEKDATE_ELEMENT]], + parts: _Parts, + ) -> None: + ... # pragma: no cover + + def _parse_token( + self, + token: Any, + value: Any, + parts: _Parts, + ) -> None: + + if token == "YYYY": + parts["year"] = int(value) + + elif token == "YY": value = int(value) - parts['year'] = 1900 + value if value > 68 else 2000 + value + parts["year"] = 1900 + value if value > 68 else 2000 + value - elif token in ['MMMM', 'MMM']: - parts['month'] = self.locale.month_number(value.lower()) + elif token in ["MMMM", "MMM"]: + # FIXME: month_number() is nullable + parts["month"] = self.locale.month_number(value.lower()) # type: ignore - elif token in ['MM', 'M']: - parts['month'] = int(value) + elif token in ["MM", "M"]: + parts["month"] = int(value) - elif token in ['DD', 'D']: - parts['day'] = int(value) + elif token in ["DDDD", "DDD"]: + parts["day_of_year"] = int(value) - elif token in ['Do']: - parts['day'] = int(value) + elif token in ["DD", "D"]: + parts["day"] = int(value) - elif token.upper() in ['HH', 'H']: - parts['hour'] = int(value) + elif token == "Do": + parts["day"] = int(value) - elif token in ['mm', 'm']: - parts['minute'] = int(value) + elif token == "dddd": + # locale day names are 1-indexed + day_of_week = [x.lower() for x in self.locale.day_names].index( + value.lower() + ) + parts["day_of_week"] = day_of_week - 1 - elif token in ['ss', 's']: - parts['second'] = int(value) + elif token == "ddd": + # locale day abbreviations are 1-indexed + day_of_week = [x.lower() for x in self.locale.day_abbreviations].index( + value.lower() + ) + parts["day_of_week"] = day_of_week - 1 - elif token == 'S': + elif token.upper() in ["HH", "H"]: + parts["hour"] = int(value) + + elif token in ["mm", "m"]: + parts["minute"] = int(value) + + elif token in ["ss", "s"]: + parts["second"] = int(value) + + elif token == "S": # We have the *most significant* digits of an arbitrary-precision integer. # We want the six most significant digits as an integer, rounded. - # FIXME: add nanosecond support somehow? - value = value.ljust(7, str('0')) + # IDEA: add nanosecond support somehow? Need datetime support for it first. + value = value.ljust(7, "0") # floating-point (IEEE-754) defaults to half-to-even rounding seventh_digit = int(value[6]) @@ -210,119 +560,220 @@ class DateTimeParser(object): else: rounding = 0 - parts['microsecond'] = int(value[:6]) + rounding + parts["microsecond"] = int(value[:6]) + rounding - elif token == 'X': - parts['timestamp'] = int(value) + elif token == "X": + parts["timestamp"] = float(value) - elif token in ['ZZZ', 'ZZ', 'Z']: - parts['tzinfo'] = TzinfoParser.parse(value) + elif token == "x": + parts["expanded_timestamp"] = int(value) - elif token in ['a', 'A']: - if value in ( - self.locale.meridians['am'], - self.locale.meridians['AM'] - ): - parts['am_pm'] = 'am' - elif value in ( - self.locale.meridians['pm'], - self.locale.meridians['PM'] - ): - parts['am_pm'] = 'pm' + elif token in ["ZZZ", "ZZ", "Z"]: + parts["tzinfo"] = TzinfoParser.parse(value) + + elif token in ["a", "A"]: + if value in (self.locale.meridians["am"], self.locale.meridians["AM"]): + parts["am_pm"] = "am" + if "hour" in parts and not 0 <= parts["hour"] <= 12: + raise ParserMatchError( + f"Hour token value must be between 0 and 12 inclusive for token {token!r}." + ) + elif value in (self.locale.meridians["pm"], self.locale.meridians["PM"]): + parts["am_pm"] = "pm" + elif token == "W": + parts["weekdate"] = value @staticmethod - def _build_datetime(parts): + def _build_datetime(parts: _Parts) -> datetime: + weekdate = parts.get("weekdate") - timestamp = parts.get('timestamp') + if weekdate is not None: - if timestamp: - tz_utc = tz.tzutc() - return datetime.fromtimestamp(timestamp, tz=tz_utc) + year, week = int(weekdate[0]), int(weekdate[1]) - am_pm = parts.get('am_pm') - hour = parts.get('hour', 0) + if weekdate[2] is not None: + _day = int(weekdate[2]) + else: + # day not given, default to 1 + _day = 1 - if am_pm == 'pm' and hour < 12: + date_string = f"{year}-{week}-{_day}" + + # tokens for ISO 8601 weekdates + dt = datetime.strptime(date_string, "%G-%V-%u") + + parts["year"] = dt.year + parts["month"] = dt.month + parts["day"] = dt.day + + timestamp = parts.get("timestamp") + + if timestamp is not None: + return datetime.fromtimestamp(timestamp, tz=tz.tzutc()) + + expanded_timestamp = parts.get("expanded_timestamp") + + if expanded_timestamp is not None: + return datetime.fromtimestamp( + normalize_timestamp(expanded_timestamp), + tz=tz.tzutc(), + ) + + day_of_year = parts.get("day_of_year") + + if day_of_year is not None: + _year = parts.get("year") + month = parts.get("month") + if _year is None: + raise ParserError( + "Year component is required with the DDD and DDDD tokens." + ) + + if month is not None: + raise ParserError( + "Month component is not allowed with the DDD and DDDD tokens." + ) + + date_string = f"{_year}-{day_of_year}" + try: + dt = datetime.strptime(date_string, "%Y-%j") + except ValueError: + raise ParserError( + f"The provided day of year {day_of_year!r} is invalid." + ) + + parts["year"] = dt.year + parts["month"] = dt.month + parts["day"] = dt.day + + day_of_week: Optional[int] = parts.get("day_of_week") + day = parts.get("day") + + # If day is passed, ignore day of week + if day_of_week is not None and day is None: + year = parts.get("year", 1970) + month = parts.get("month", 1) + day = 1 + + # dddd => first day of week after epoch + # dddd YYYY => first day of week in specified year + # dddd MM YYYY => first day of week in specified year and month + # dddd MM => first day after epoch in specified month + next_weekday_dt = next_weekday(datetime(year, month, day), day_of_week) + parts["year"] = next_weekday_dt.year + parts["month"] = next_weekday_dt.month + parts["day"] = next_weekday_dt.day + + am_pm = parts.get("am_pm") + hour = parts.get("hour", 0) + + if am_pm == "pm" and hour < 12: hour += 12 - elif am_pm == 'am' and hour == 12: + elif am_pm == "am" and hour == 12: hour = 0 - return datetime(year=parts.get('year', 1), month=parts.get('month', 1), - day=parts.get('day', 1), hour=hour, minute=parts.get('minute', 0), - second=parts.get('second', 0), microsecond=parts.get('microsecond', 0), - tzinfo=parts.get('tzinfo')) + # Support for midnight at the end of day + if hour == 24: + if parts.get("minute", 0) != 0: + raise ParserError("Midnight at the end of day must not contain minutes") + if parts.get("second", 0) != 0: + raise ParserError("Midnight at the end of day must not contain seconds") + if parts.get("microsecond", 0) != 0: + raise ParserError( + "Midnight at the end of day must not contain microseconds" + ) + hour = 0 + day_increment = 1 + else: + day_increment = 0 - def _parse_multiformat(self, string, formats): + # account for rounding up to 1000000 + microsecond = parts.get("microsecond", 0) + if microsecond == 1000000: + microsecond = 0 + second_increment = 1 + else: + second_increment = 0 - _datetime = None + increment = timedelta(days=day_increment, seconds=second_increment) + + return ( + datetime( + year=parts.get("year", 1), + month=parts.get("month", 1), + day=parts.get("day", 1), + hour=hour, + minute=parts.get("minute", 0), + second=parts.get("second", 0), + microsecond=microsecond, + tzinfo=parts.get("tzinfo"), + ) + + increment + ) + + def _parse_multiformat(self, string: str, formats: Iterable[str]) -> datetime: + + _datetime: Optional[datetime] = None for fmt in formats: try: _datetime = self.parse(string, fmt) break - except ParserError: + except ParserMatchError: pass if _datetime is None: - raise ParserError('Could not match input to any of {0} on \'{1}\''.format(formats, string)) + supported_formats = ", ".join(formats) + raise ParserError( + f"Could not match input {string!r} to any of the following formats: {supported_formats}." + ) return _datetime + # generates a capture group of choices separated by an OR operator @staticmethod - def _map_lookup(input_map, key): - - try: - return input_map[key] - except KeyError: - raise ParserError('Could not match "{0}" to {1}'.format(key, input_map)) - - @staticmethod - def _try_timestamp(string): - - try: - return float(string) - except: - return None - - @staticmethod - def _choice_re(choices, flags=0): - return re.compile('({0})'.format('|'.join(choices)), flags=flags) + def _generate_choice_re( + choices: Iterable[str], flags: Union[int, re.RegexFlag] = 0 + ) -> Pattern[str]: + return re.compile(r"({})".format("|".join(choices)), flags=flags) -class TzinfoParser(object): - - _TZINFO_RE = re.compile('([+\-])?(\d\d):?(\d\d)?') +class TzinfoParser: + _TZINFO_RE: ClassVar[Pattern[str]] = re.compile( + r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$" + ) @classmethod - def parse(cls, string): + def parse(cls, tzinfo_string: str) -> dt_tzinfo: - tzinfo = None + tzinfo: Optional[dt_tzinfo] = None - if string == 'local': + if tzinfo_string == "local": tzinfo = tz.tzlocal() - elif string in ['utc', 'UTC']: + elif tzinfo_string in ["utc", "UTC", "Z"]: tzinfo = tz.tzutc() else: - iso_match = cls._TZINFO_RE.match(string) + iso_match = cls._TZINFO_RE.match(tzinfo_string) if iso_match: + sign: Optional[str] + hours: str + minutes: Union[str, int, None] sign, hours, minutes = iso_match.groups() - if minutes is None: - minutes = 0 - seconds = int(hours) * 3600 + int(minutes) * 60 + seconds = int(hours) * 3600 + int(minutes or 0) * 60 - if sign == '-': + if sign == "-": seconds *= -1 tzinfo = tz.tzoffset(None, seconds) else: - tzinfo = tz.gettz(string) + tzinfo = tz.gettz(tzinfo_string) if tzinfo is None: - raise ParserError('Could not parse timezone expression "{0}"'.format(string)) + raise ParserError(f"Could not parse timezone expression {tzinfo_string!r}.") return tzinfo diff --git a/lib/arrow/py.typed b/lib/arrow/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/lib/arrow/util.py b/lib/arrow/util.py index 3eed4faa..f3eaa21c 100644 --- a/lib/arrow/util.py +++ b/lib/arrow/util.py @@ -1,47 +1,117 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import +"""Helpful functions used internally within arrow.""" -import sys +import datetime +from typing import Any, Optional, cast -# python 2.6 / 2.7 definitions for total_seconds function. +from dateutil.rrule import WEEKLY, rrule -def _total_seconds_27(td): # pragma: no cover - return td.total_seconds() - -def _total_seconds_26(td): - return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 1e6) / 1e6 +from arrow.constants import ( + MAX_ORDINAL, + MAX_TIMESTAMP, + MAX_TIMESTAMP_MS, + MAX_TIMESTAMP_US, + MIN_ORDINAL, +) -# get version info and assign correct total_seconds function. +def next_weekday( + start_date: Optional[datetime.date], weekday: int +) -> datetime.datetime: + """Get next weekday from the specified start date. -version = '{0}.{1}.{2}'.format(*sys.version_info[:3]) + :param start_date: Datetime object representing the start date. + :param weekday: Next weekday to obtain. Can be a value between 0 (Monday) and 6 (Sunday). + :return: Datetime object corresponding to the next weekday after start_date. -if version < '2.7': # pragma: no cover - total_seconds = _total_seconds_26 -else: # pragma: no cover - total_seconds = _total_seconds_27 + Usage:: -def is_timestamp(value): - if type(value) == bool: + # Get first Monday after epoch + >>> next_weekday(datetime(1970, 1, 1), 0) + 1970-01-05 00:00:00 + + # Get first Thursday after epoch + >>> next_weekday(datetime(1970, 1, 1), 3) + 1970-01-01 00:00:00 + + # Get first Sunday after epoch + >>> next_weekday(datetime(1970, 1, 1), 6) + 1970-01-04 00:00:00 + """ + if weekday < 0 or weekday > 6: + raise ValueError("Weekday must be between 0 (Monday) and 6 (Sunday).") + return cast( + datetime.datetime, + rrule(freq=WEEKLY, dtstart=start_date, byweekday=weekday, count=1)[0], + ) + + +def is_timestamp(value: Any) -> bool: + """Check if value is a valid timestamp.""" + if isinstance(value, bool): + return False + if not isinstance(value, (int, float, str)): return False try: float(value) return True - except: + except ValueError: return False -# python 2.7 / 3.0+ definitions for isstr function. -try: # pragma: no cover - basestring +def validate_ordinal(value: Any) -> None: + """Raise an exception if value is an invalid Gregorian ordinal. - def isstr(s): - return isinstance(s, basestring) + :param value: the input to be checked -except NameError: #pragma: no cover - - def isstr(s): - return isinstance(s, str) + """ + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"Ordinal must be an integer (got type {type(value)}).") + if not (MIN_ORDINAL <= value <= MAX_ORDINAL): + raise ValueError(f"Ordinal {value} is out of range.") -__all__ = ['total_seconds', 'is_timestamp', 'isstr'] +def normalize_timestamp(timestamp: float) -> float: + """Normalize millisecond and microsecond timestamps into normal timestamps.""" + if timestamp > MAX_TIMESTAMP: + if timestamp < MAX_TIMESTAMP_MS: + timestamp /= 1000 + elif timestamp < MAX_TIMESTAMP_US: + timestamp /= 1_000_000 + else: + raise ValueError(f"The specified timestamp {timestamp!r} is too large.") + return timestamp + + +# Credit to https://stackoverflow.com/a/1700069 +def iso_to_gregorian(iso_year: int, iso_week: int, iso_day: int) -> datetime.date: + """Converts an ISO week date into a datetime object. + + :param iso_year: the year + :param iso_week: the week number, each year has either 52 or 53 weeks + :param iso_day: the day numbered 1 through 7, beginning with Monday + + """ + + if not 1 <= iso_week <= 53: + raise ValueError("ISO Calendar week value must be between 1-53.") + + if not 1 <= iso_day <= 7: + raise ValueError("ISO Calendar day value must be between 1-7") + + # The first week of the year always contains 4 Jan. + fourth_jan = datetime.date(iso_year, 1, 4) + delta = datetime.timedelta(fourth_jan.isoweekday() - 1) + year_start = fourth_jan - delta + gregorian = year_start + datetime.timedelta(days=iso_day - 1, weeks=iso_week - 1) + + return gregorian + + +def validate_bounds(bounds: str) -> None: + if bounds != "()" and bounds != "(]" and bounds != "[)" and bounds != "[]": + raise ValueError( + "Invalid bounds. Please select between '()', '(]', '[)', or '[]'." + ) + + +__all__ = ["next_weekday", "is_timestamp", "validate_ordinal", "iso_to_gregorian"]