diff options
Diffstat (limited to 'src')
114 files changed, 13086 insertions, 0 deletions
diff --git a/src/pendulum/__init__.py b/src/pendulum/__init__.py new file mode 100644 index 0000000..3863b76 --- /dev/null +++ b/src/pendulum/__init__.py @@ -0,0 +1,402 @@ +from __future__ import annotations + +import datetime as _datetime + +from typing import Union +from typing import cast +from typing import overload + +from pendulum.__version__ import __version__ +from pendulum.constants import DAYS_PER_WEEK +from pendulum.constants import HOURS_PER_DAY +from pendulum.constants import MINUTES_PER_HOUR +from pendulum.constants import MONTHS_PER_YEAR +from pendulum.constants import SECONDS_PER_DAY +from pendulum.constants import SECONDS_PER_HOUR +from pendulum.constants import SECONDS_PER_MINUTE +from pendulum.constants import WEEKS_PER_YEAR +from pendulum.constants import YEARS_PER_CENTURY +from pendulum.constants import YEARS_PER_DECADE +from pendulum.date import Date +from pendulum.datetime import DateTime +from pendulum.day import WeekDay +from pendulum.duration import Duration +from pendulum.formatting import Formatter +from pendulum.helpers import format_diff +from pendulum.helpers import get_locale +from pendulum.helpers import locale +from pendulum.helpers import set_locale +from pendulum.helpers import week_ends_at +from pendulum.helpers import week_starts_at +from pendulum.interval import Interval +from pendulum.parser import parse +from pendulum.testing.traveller import Traveller +from pendulum.time import Time +from pendulum.tz import UTC +from pendulum.tz import fixed_timezone +from pendulum.tz import local_timezone +from pendulum.tz import set_local_timezone +from pendulum.tz import test_local_timezone +from pendulum.tz import timezones +from pendulum.tz.timezone import FixedTimezone +from pendulum.tz.timezone import Timezone + + +MONDAY = WeekDay.MONDAY +TUESDAY = WeekDay.TUESDAY +WEDNESDAY = WeekDay.WEDNESDAY +THURSDAY = WeekDay.THURSDAY +FRIDAY = WeekDay.FRIDAY +SATURDAY = WeekDay.SATURDAY +SUNDAY = WeekDay.SUNDAY + +_TEST_NOW: DateTime | None = None +_LOCALE = "en" +_WEEK_STARTS_AT: WeekDay = WeekDay.MONDAY +_WEEK_ENDS_AT: WeekDay = WeekDay.SUNDAY + +_formatter = Formatter() + + +@overload +def timezone(name: int) -> FixedTimezone: + ... + + +@overload +def timezone(name: str) -> Timezone: + ... + + +@overload +def timezone(name: str | int) -> Timezone | FixedTimezone: + ... + + +def timezone(name: str | int) -> Timezone | FixedTimezone: + """ + Return a Timezone instance given its name. + """ + if isinstance(name, int): + return fixed_timezone(name) + + if name.lower() == "utc": + return UTC + + return Timezone(name) + + +def _safe_timezone( + obj: str | float | _datetime.tzinfo | Timezone | FixedTimezone | None, + dt: _datetime.datetime | None = None, +) -> Timezone | FixedTimezone: + """ + Creates a timezone instance + from a string, Timezone, TimezoneInfo or integer offset. + """ + if isinstance(obj, (Timezone, FixedTimezone)): + return obj + + if obj is None or obj == "local": + return local_timezone() + + if isinstance(obj, (int, float)): + obj = int(obj * 60 * 60) + elif isinstance(obj, _datetime.tzinfo): + # zoneinfo + if hasattr(obj, "key"): + obj = obj.key + # pytz + elif hasattr(obj, "localize"): + obj = obj.zone # type: ignore[attr-defined] + elif obj.tzname(None) == "UTC": + return UTC + else: + offset = obj.utcoffset(dt) + + if offset is None: + offset = _datetime.timedelta(0) + + obj = int(offset.total_seconds()) + + obj = cast(Union[str, int], obj) + + return timezone(obj) + + +# Public API +def datetime( + year: int, + month: int, + day: int, + hour: int = 0, + minute: int = 0, + second: int = 0, + microsecond: int = 0, + tz: str | float | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, + fold: int = 1, + raise_on_unknown_times: bool = False, +) -> DateTime: + """ + Creates a new DateTime instance from a specific date and time. + """ + return DateTime.create( + year, + month, + day, + hour=hour, + minute=minute, + second=second, + microsecond=microsecond, + tz=tz, + fold=fold, + raise_on_unknown_times=raise_on_unknown_times, + ) + + +def local( + year: int, + month: int, + day: int, + hour: int = 0, + minute: int = 0, + second: int = 0, + microsecond: int = 0, +) -> DateTime: + """ + Return a DateTime in the local timezone. + """ + return datetime( + year, month, day, hour, minute, second, microsecond, tz=local_timezone() + ) + + +def naive( + year: int, + month: int, + day: int, + hour: int = 0, + minute: int = 0, + second: int = 0, + microsecond: int = 0, + fold: int = 1, +) -> DateTime: + """ + Return a naive DateTime. + """ + return DateTime(year, month, day, hour, minute, second, microsecond, fold=fold) + + +def date(year: int, month: int, day: int) -> Date: + """ + Create a new Date instance. + """ + return Date(year, month, day) + + +def time(hour: int, minute: int = 0, second: int = 0, microsecond: int = 0) -> Time: + """ + Create a new Time instance. + """ + return Time(hour, minute, second, microsecond) + + +@overload +def instance( + obj: _datetime.datetime, + tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, +) -> DateTime: + ... + + +@overload +def instance( + obj: _datetime.date, + tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, +) -> Date: + ... + + +@overload +def instance( + obj: _datetime.time, + tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, +) -> Time: + ... + + +def instance( + obj: _datetime.datetime | _datetime.date | _datetime.time, + tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, +) -> DateTime | Date | Time: + """ + Create a DateTime/Date/Time instance from a datetime/date/time native one. + """ + if isinstance(obj, (DateTime, Date, Time)): + return obj + + if isinstance(obj, _datetime.date) and not isinstance(obj, _datetime.datetime): + return date(obj.year, obj.month, obj.day) + + if isinstance(obj, _datetime.time): + return Time.instance(obj, tz=tz) + + return DateTime.instance(obj, tz=tz) + + +def now(tz: str | Timezone | None = None) -> DateTime: + """ + Get a DateTime instance for the current date and time. + """ + return DateTime.now(tz) + + +def today(tz: str | Timezone = "local") -> DateTime: + """ + Create a DateTime instance for today. + """ + return now(tz).start_of("day") + + +def tomorrow(tz: str | Timezone = "local") -> DateTime: + """ + Create a DateTime instance for tomorrow. + """ + return today(tz).add(days=1) + + +def yesterday(tz: str | Timezone = "local") -> DateTime: + """ + Create a DateTime instance for yesterday. + """ + return today(tz).subtract(days=1) + + +def from_format( + string: str, + fmt: str, + tz: str | Timezone = UTC, + locale: str | None = None, +) -> DateTime: + """ + Creates a DateTime instance from a specific format. + """ + parts = _formatter.parse(string, fmt, now(tz=tz), locale=locale) + if parts["tz"] is None: + parts["tz"] = tz + + return datetime(**parts) + + +def from_timestamp(timestamp: int | float, tz: str | Timezone = UTC) -> DateTime: + """ + Create a DateTime instance from a timestamp. + """ + dt = _datetime.datetime.utcfromtimestamp(timestamp) + + dt = datetime( + dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond + ) + + if tz is not UTC or tz != "UTC": + dt = dt.in_timezone(tz) + + return dt + + +def duration( + days: float = 0, + seconds: float = 0, + microseconds: float = 0, + milliseconds: float = 0, + minutes: float = 0, + hours: float = 0, + weeks: float = 0, + years: float = 0, + months: float = 0, +) -> Duration: + """ + Create a Duration instance. + """ + return Duration( + days=days, + seconds=seconds, + microseconds=microseconds, + milliseconds=milliseconds, + minutes=minutes, + hours=hours, + weeks=weeks, + years=years, + months=months, + ) + + +def interval(start: DateTime, end: DateTime, absolute: bool = False) -> Interval: + """ + Create an Interval instance. + """ + return Interval(start, end, absolute=absolute) + + +# Testing + +_traveller = Traveller(DateTime) + +freeze = _traveller.freeze +travel = _traveller.travel +travel_to = _traveller.travel_to +travel_back = _traveller.travel_back + +__all__ = [ + "__version__", + "DAYS_PER_WEEK", + "HOURS_PER_DAY", + "MINUTES_PER_HOUR", + "MONTHS_PER_YEAR", + "SECONDS_PER_DAY", + "SECONDS_PER_HOUR", + "SECONDS_PER_MINUTE", + "WEEKS_PER_YEAR", + "YEARS_PER_CENTURY", + "YEARS_PER_DECADE", + "Date", + "DateTime", + "Duration", + "Formatter", + "WeekDay", + "date", + "datetime", + "duration", + "format_diff", + "freeze", + "from_format", + "from_timestamp", + "get_locale", + "instance", + "interval", + "local", + "locale", + "naive", + "now", + "set_locale", + "week_ends_at", + "week_starts_at", + "parse", + "Interval", + "Time", + "UTC", + "local_timezone", + "set_local_timezone", + "test_local_timezone", + "time", + "timezone", + "timezones", + "today", + "tomorrow", + "travel", + "travel_back", + "travel_to", + "FixedTimezone", + "Timezone", + "yesterday", +] diff --git a/src/pendulum/__version__.py b/src/pendulum/__version__.py new file mode 100644 index 0000000..d6e60fe --- /dev/null +++ b/src/pendulum/__version__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + + +__version__ = "3.0.0" diff --git a/src/pendulum/_helpers.py b/src/pendulum/_helpers.py new file mode 100644 index 0000000..5f7bd03 --- /dev/null +++ b/src/pendulum/_helpers.py @@ -0,0 +1,335 @@ +from __future__ import annotations + +import datetime +import math + +from typing import NamedTuple +from typing import cast + +from pendulum.constants import DAY_OF_WEEK_TABLE +from pendulum.constants import DAYS_PER_L_YEAR +from pendulum.constants import DAYS_PER_MONTHS +from pendulum.constants import DAYS_PER_N_YEAR +from pendulum.constants import EPOCH_YEAR +from pendulum.constants import MONTHS_OFFSETS +from pendulum.constants import SECS_PER_4_YEARS +from pendulum.constants import SECS_PER_100_YEARS +from pendulum.constants import SECS_PER_400_YEARS +from pendulum.constants import SECS_PER_DAY +from pendulum.constants import SECS_PER_HOUR +from pendulum.constants import SECS_PER_MIN +from pendulum.constants import SECS_PER_YEAR +from pendulum.constants import TM_DECEMBER +from pendulum.constants import TM_JANUARY +from pendulum.tz.timezone import Timezone +from pendulum.utils._compat import zoneinfo + + +class PreciseDiff(NamedTuple): + years: int + months: int + days: int + hours: int + minutes: int + seconds: int + microseconds: int + total_days: int + + def __repr__(self) -> str: + return ( + f"{self.years} years " + f"{self.months} months " + f"{self.days} days " + f"{self.hours} hours " + f"{self.minutes} minutes " + f"{self.seconds} seconds " + f"{self.microseconds} microseconds" + ) + + +def is_leap(year: int) -> bool: + return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) + + +def is_long_year(year: int) -> bool: + def p(y: int) -> int: + return y + y // 4 - y // 100 + y // 400 + + return p(year) % 7 == 4 or p(year - 1) % 7 == 3 + + +def week_day(year: int, month: int, day: int) -> int: + if month < 3: + year -= 1 + + w = ( + year + + year // 4 + - year // 100 + + year // 400 + + DAY_OF_WEEK_TABLE[month - 1] + + day + ) % 7 + + if not w: + w = 7 + + return w + + +def days_in_year(year: int) -> int: + if is_leap(year): + return DAYS_PER_L_YEAR + + return DAYS_PER_N_YEAR + + +def local_time( + unix_time: int, utc_offset: int, microseconds: int +) -> tuple[int, int, int, int, int, int, int]: + """ + Returns a UNIX time as a broken-down time + for a particular transition type. + """ + year = EPOCH_YEAR + seconds = math.floor(unix_time) + + # Shift to a base year that is 400-year aligned. + if seconds >= 0: + seconds -= 10957 * SECS_PER_DAY + year += 30 # == 2000 + else: + seconds += (146097 - 10957) * SECS_PER_DAY + year -= 370 # == 1600 + + seconds += utc_offset + + # Handle years in chunks of 400/100/4/1 + year += 400 * (seconds // SECS_PER_400_YEARS) + seconds %= SECS_PER_400_YEARS + if seconds < 0: + seconds += SECS_PER_400_YEARS + year -= 400 + + leap_year = 1 # 4-century aligned + + sec_per_100years = SECS_PER_100_YEARS[leap_year] + while seconds >= sec_per_100years: + seconds -= sec_per_100years + year += 100 + leap_year = 0 # 1-century, non 4-century aligned + sec_per_100years = SECS_PER_100_YEARS[leap_year] + + sec_per_4years = SECS_PER_4_YEARS[leap_year] + while seconds >= sec_per_4years: + seconds -= sec_per_4years + year += 4 + leap_year = 1 # 4-year, non century aligned + sec_per_4years = SECS_PER_4_YEARS[leap_year] + + sec_per_year = SECS_PER_YEAR[leap_year] + while seconds >= sec_per_year: + seconds -= sec_per_year + year += 1 + leap_year = 0 # non 4-year aligned + sec_per_year = SECS_PER_YEAR[leap_year] + + # Handle months and days + month = TM_DECEMBER + 1 + day = seconds // SECS_PER_DAY + 1 + seconds %= SECS_PER_DAY + while month != TM_JANUARY + 1: + month_offset = MONTHS_OFFSETS[leap_year][month] + if day > month_offset: + day -= month_offset + break + + month -= 1 + + # Handle hours, minutes, seconds and microseconds + hour, seconds = divmod(seconds, SECS_PER_HOUR) + minute, second = divmod(seconds, SECS_PER_MIN) + + return year, month, day, hour, minute, second, microseconds + + +def precise_diff( + d1: datetime.datetime | datetime.date, d2: datetime.datetime | datetime.date +) -> PreciseDiff: + """ + Calculate a precise difference between two datetimes. + + :param d1: The first datetime + :param d2: The second datetime + """ + sign = 1 + + if d1 == d2: + return PreciseDiff(0, 0, 0, 0, 0, 0, 0, 0) + + tzinfo1: datetime.tzinfo | None = ( + d1.tzinfo if isinstance(d1, datetime.datetime) else None + ) + tzinfo2: datetime.tzinfo | None = ( + d2.tzinfo if isinstance(d2, datetime.datetime) else None + ) + + if ( + tzinfo1 is None + and tzinfo2 is not None + or tzinfo2 is None + and tzinfo1 is not None + ): + raise ValueError( + "Comparison between naive and aware datetimes is not supported" + ) + + if d1 > d2: + d1, d2 = d2, d1 + sign = -1 + + d_diff = 0 + hour_diff = 0 + min_diff = 0 + sec_diff = 0 + mic_diff = 0 + total_days = _day_number(d2.year, d2.month, d2.day) - _day_number( + d1.year, d1.month, d1.day + ) + in_same_tz = False + tz1 = None + tz2 = None + + # Trying to figure out the timezone names + # If we can't find them, we assume different timezones + if tzinfo1 and tzinfo2: + tz1 = _get_tzinfo_name(tzinfo1) + tz2 = _get_tzinfo_name(tzinfo2) + + in_same_tz = tz1 == tz2 and tz1 is not None + + if isinstance(d2, datetime.datetime): + if isinstance(d1, datetime.datetime): + # If we are not in the same timezone + # we need to adjust + # + # We also need to adjust if we do not + # have variable-length units + if not in_same_tz or total_days == 0: + offset1 = d1.utcoffset() + offset2 = d2.utcoffset() + + if offset1: + d1 = d1 - offset1 + + if offset2: + d2 = d2 - offset2 + + hour_diff = d2.hour - d1.hour + min_diff = d2.minute - d1.minute + sec_diff = d2.second - d1.second + mic_diff = d2.microsecond - d1.microsecond + else: + hour_diff = d2.hour + min_diff = d2.minute + sec_diff = d2.second + mic_diff = d2.microsecond + + if mic_diff < 0: + mic_diff += 1000000 + sec_diff -= 1 + + if sec_diff < 0: + sec_diff += 60 + min_diff -= 1 + + if min_diff < 0: + min_diff += 60 + hour_diff -= 1 + + if hour_diff < 0: + hour_diff += 24 + d_diff -= 1 + + y_diff = d2.year - d1.year + m_diff = d2.month - d1.month + d_diff += d2.day - d1.day + + if d_diff < 0: + year = d2.year + month = d2.month + + if month == 1: + month = 12 + year -= 1 + else: + month -= 1 + + leap = int(is_leap(year)) + + days_in_last_month = DAYS_PER_MONTHS[leap][month] + days_in_month = DAYS_PER_MONTHS[int(is_leap(d2.year))][d2.month] + + if d_diff < days_in_month - days_in_last_month: + # We don't have a full month, we calculate days + if days_in_last_month < d1.day: + d_diff += d1.day + else: + d_diff += days_in_last_month + elif d_diff == days_in_month - days_in_last_month: + # We have exactly a full month + # We remove the days difference + # and add one to the months difference + d_diff = 0 + m_diff += 1 + else: + # We have a full month + d_diff += days_in_last_month + + m_diff -= 1 + + if m_diff < 0: + m_diff += 12 + y_diff -= 1 + + return PreciseDiff( + sign * y_diff, + sign * m_diff, + sign * d_diff, + sign * hour_diff, + sign * min_diff, + sign * sec_diff, + sign * mic_diff, + sign * total_days, + ) + + +def _day_number(year: int, month: int, day: int) -> int: + month = (month + 9) % 12 + year = year - month // 10 + + return ( + 365 * year + + year // 4 + - year // 100 + + year // 400 + + (month * 306 + 5) // 10 + + (day - 1) + ) + + +def _get_tzinfo_name(tzinfo: datetime.tzinfo | None) -> str | None: + if tzinfo is None: + return None + + if hasattr(tzinfo, "key"): + # zoneinfo timezone + return cast(zoneinfo.ZoneInfo, tzinfo).key + elif hasattr(tzinfo, "name"): + # Pendulum timezone + return cast(Timezone, tzinfo).name + elif hasattr(tzinfo, "zone"): + # pytz timezone + return tzinfo.zone # type: ignore[no-any-return] + + return None diff --git a/src/pendulum/_pendulum.pyi b/src/pendulum/_pendulum.pyi new file mode 100644 index 0000000..74d7d83 --- /dev/null +++ b/src/pendulum/_pendulum.pyi @@ -0,0 +1,40 @@ +from __future__ import annotations + +from datetime import date +from datetime import datetime +from datetime import time +from typing import NamedTuple + +class Duration: + years: int = 0 + months: int = 0 + weeks: int = 0 + days: int = 0 + remaining_days: int = 0 + hours: int = 0 + minutes: int = 0 + seconds: int = 0 + remaining_seconds: int = 0 + microseconds: int = 0 + +class PreciseDiff(NamedTuple): + years: int + months: int + days: int + hours: int + minutes: int + seconds: int + microseconds: int + total_days: int + +def parse_iso8601( + text: str, +) -> datetime | date | time | Duration: ... +def days_in_year(year: int) -> int: ... +def is_leap(year: int) -> bool: ... +def is_long_year(year: int) -> bool: ... +def local_time( + unix_time: int, utc_offset: int, microseconds: int +) -> tuple[int, int, int, int, int, int, int]: ... +def precise_diff(d1: datetime | date, d2: datetime | date) -> PreciseDiff: ... +def week_day(year: int, month: int, day: int) -> int: ... diff --git a/src/pendulum/constants.py b/src/pendulum/constants.py new file mode 100644 index 0000000..51eb059 --- /dev/null +++ b/src/pendulum/constants.py @@ -0,0 +1,102 @@ +# The day constants +from __future__ import annotations + + +# Number of X in Y. +YEARS_PER_CENTURY = 100 +YEARS_PER_DECADE = 10 +MONTHS_PER_YEAR = 12 +WEEKS_PER_YEAR = 52 +DAYS_PER_WEEK = 7 +HOURS_PER_DAY = 24 +MINUTES_PER_HOUR = 60 +SECONDS_PER_MINUTE = 60 +SECONDS_PER_HOUR = MINUTES_PER_HOUR * SECONDS_PER_MINUTE +SECONDS_PER_DAY = HOURS_PER_DAY * SECONDS_PER_HOUR +US_PER_SECOND = 1000000 + +# Formats +ATOM = "YYYY-MM-DDTHH:mm:ssZ" +COOKIE = "dddd, DD-MMM-YYYY HH:mm:ss zz" +ISO8601 = "YYYY-MM-DDTHH:mm:ssZ" +ISO8601_EXTENDED = "YYYY-MM-DDTHH:mm:ss.SSSSSSZ" +RFC822 = "ddd, DD MMM YY HH:mm:ss ZZ" +RFC850 = "dddd, DD-MMM-YY HH:mm:ss zz" +RFC1036 = "ddd, DD MMM YY HH:mm:ss ZZ" +RFC1123 = "ddd, DD MMM YYYY HH:mm:ss ZZ" +RFC2822 = "ddd, DD MMM YYYY HH:mm:ss ZZ" +RFC3339 = ISO8601 +RFC3339_EXTENDED = ISO8601_EXTENDED +RSS = "ddd, DD MMM YYYY HH:mm:ss ZZ" +W3C = ISO8601 + + +EPOCH_YEAR = 1970 + +DAYS_PER_N_YEAR = 365 +DAYS_PER_L_YEAR = 366 + +USECS_PER_SEC = 1000000 + +SECS_PER_MIN = 60 +SECS_PER_HOUR = 60 * SECS_PER_MIN +SECS_PER_DAY = SECS_PER_HOUR * 24 + +# 400-year chunks always have 146097 days (20871 weeks). +SECS_PER_400_YEARS = 146097 * SECS_PER_DAY + +# The number of seconds in an aligned 100-year chunk, for those that +# do not begin with a leap year and those that do respectively. +SECS_PER_100_YEARS = ( + (76 * DAYS_PER_N_YEAR + 24 * DAYS_PER_L_YEAR) * SECS_PER_DAY, + (75 * DAYS_PER_N_YEAR + 25 * DAYS_PER_L_YEAR) * SECS_PER_DAY, +) + +# The number of seconds in an aligned 4-year chunk, for those that +# do not begin with a leap year and those that do respectively. +SECS_PER_4_YEARS = ( + (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY, + (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY, +) + +# The number of seconds in non-leap and leap years respectively. +SECS_PER_YEAR = (DAYS_PER_N_YEAR * SECS_PER_DAY, DAYS_PER_L_YEAR * SECS_PER_DAY) + +DAYS_PER_YEAR = (DAYS_PER_N_YEAR, DAYS_PER_L_YEAR) + +# The month lengths in non-leap and leap years respectively. +DAYS_PER_MONTHS = ( + (-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31), + (-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31), +) + +# The day offsets of the beginning of each (1-based) month in non-leap +# and leap years respectively. +# For example, in a leap year there are 335 days before December. +MONTHS_OFFSETS = ( + (-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365), + (-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366), +) + +DAY_OF_WEEK_TABLE = (0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4) + +TM_SUNDAY = 0 +TM_MONDAY = 1 +TM_TUESDAY = 2 +TM_WEDNESDAY = 3 +TM_THURSDAY = 4 +TM_FRIDAY = 5 +TM_SATURDAY = 6 + +TM_JANUARY = 0 +TM_FEBRUARY = 1 +TM_MARCH = 2 +TM_APRIL = 3 +TM_MAY = 4 +TM_JUNE = 5 +TM_JULY = 6 +TM_AUGUST = 7 +TM_SEPTEMBER = 8 +TM_OCTOBER = 9 +TM_NOVEMBER = 10 +TM_DECEMBER = 11 diff --git a/src/pendulum/date.py b/src/pendulum/date.py new file mode 100644 index 0000000..e7b862c --- /dev/null +++ b/src/pendulum/date.py @@ -0,0 +1,760 @@ +# The following is only needed because of Python 3.7 +# mypy: no-warn-unused-ignores +from __future__ import annotations + +import calendar +import math + +from datetime import date +from datetime import datetime +from datetime import timedelta +from typing import TYPE_CHECKING +from typing import ClassVar +from typing import NoReturn +from typing import cast +from typing import overload + +import pendulum + +from pendulum.constants import MONTHS_PER_YEAR +from pendulum.constants import YEARS_PER_CENTURY +from pendulum.constants import YEARS_PER_DECADE +from pendulum.day import WeekDay +from pendulum.exceptions import PendulumException +from pendulum.helpers import add_duration +from pendulum.interval import Interval +from pendulum.mixins.default import FormattableMixin + + +if TYPE_CHECKING: + from typing_extensions import Self + from typing_extensions import SupportsIndex + + +class Date(FormattableMixin, date): + _MODIFIERS_VALID_UNITS: ClassVar[list[str]] = [ + "day", + "week", + "month", + "year", + "decade", + "century", + ] + + # Getters/Setters + + def set( + self, year: int | None = None, month: int | None = None, day: int | None = None + ) -> Self: + return self.replace(year=year, month=month, day=day) + + @property + def day_of_week(self) -> WeekDay: + """ + Returns the day of the week (0-6). + """ + return WeekDay(self.weekday()) + + @property + def day_of_year(self) -> int: + """ + Returns the day of the year (1-366). + """ + k = 1 if self.is_leap_year() else 2 + + return (275 * self.month) // 9 - k * ((self.month + 9) // 12) + self.day - 30 + + @property + def week_of_year(self) -> int: + return self.isocalendar()[1] + + @property + def days_in_month(self) -> int: + return calendar.monthrange(self.year, self.month)[1] + + @property + def week_of_month(self) -> int: + return math.ceil((self.day + self.first_of("month").isoweekday() - 1) / 7) + + @property + def age(self) -> int: + return self.diff(abs=False).in_years() + + @property + def quarter(self) -> int: + return math.ceil(self.month / 3) + + # String Formatting + + def to_date_string(self) -> str: + """ + Format the instance as date. + + :rtype: str + """ + return self.strftime("%Y-%m-%d") + + def to_formatted_date_string(self) -> str: + """ + Format the instance as a readable date. + + :rtype: str + """ + return self.strftime("%b %d, %Y") + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.year}, {self.month}, {self.day})" + + # COMPARISONS + + def closest(self, dt1: date, dt2: date) -> Self: + """ + Get the closest date from the instance. + """ + dt1 = self.__class__(dt1.year, dt1.month, dt1.day) + dt2 = self.__class__(dt2.year, dt2.month, dt2.day) + + if self.diff(dt1).in_seconds() < self.diff(dt2).in_seconds(): + return dt1 + + return dt2 + + def farthest(self, dt1: date, dt2: date) -> Self: + """ + Get the farthest date from the instance. + """ + dt1 = self.__class__(dt1.year, dt1.month, dt1.day) + dt2 = self.__class__(dt2.year, dt2.month, dt2.day) + + if self.diff(dt1).in_seconds() > self.diff(dt2).in_seconds(): + return dt1 + + return dt2 + + def is_future(self) -> bool: + """ + Determines if the instance is in the future, ie. greater than now. + """ + return self > self.today() + + def is_past(self) -> bool: + """ + Determines if the instance is in the past, ie. less than now. + """ + return self < self.today() + + def is_leap_year(self) -> bool: + """ + Determines if the instance is a leap year. + """ + return calendar.isleap(self.year) + + def is_long_year(self) -> bool: + """ + Determines if the instance is a long year + + See link `<https://en.wikipedia.org/wiki/ISO_8601#Week_dates>`_ + """ + return Date(self.year, 12, 28).isocalendar()[1] == 53 + + def is_same_day(self, dt: date) -> bool: + """ + Checks if the passed in date is the same day as the instance current day. + """ + return self == dt + + def is_anniversary(self, dt: date | None = None) -> bool: + """ + Check if it's the anniversary. + + Compares the date/month values of the two dates. + """ + if dt is None: + dt = self.__class__.today() + + instance = self.__class__(dt.year, dt.month, dt.day) + + return (self.month, self.day) == (instance.month, instance.day) + + # the additional method for checking if today is the anniversary day + # the alias is provided to start using a new name and keep the backward + # compatibility the old name can be completely replaced with the new in + # one of the future versions + is_birthday = is_anniversary + + # ADDITIONS AND SUBTRACTIONS + + def add( + self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0 + ) -> Self: + """ + Add duration to the instance. + + :param years: The number of years + :param months: The number of months + :param weeks: The number of weeks + :param days: The number of days + """ + dt = add_duration( + date(self.year, self.month, self.day), + years=years, + months=months, + weeks=weeks, + days=days, + ) + + return self.__class__(dt.year, dt.month, dt.day) + + def subtract( + self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0 + ) -> Self: + """ + Remove duration from the instance. + + :param years: The number of years + :param months: The number of months + :param weeks: The number of weeks + :param days: The number of days + """ + return self.add(years=-years, months=-months, weeks=-weeks, days=-days) + + def _add_timedelta(self, delta: timedelta) -> Self: + """ + Add timedelta duration to the instance. + + :param delta: The timedelta instance + """ + if isinstance(delta, pendulum.Duration): + return self.add( + years=delta.years, + months=delta.months, + weeks=delta.weeks, + days=delta.remaining_days, + ) + + return self.add(days=delta.days) + + def _subtract_timedelta(self, delta: timedelta) -> Self: + """ + Remove timedelta duration from the instance. + + :param delta: The timedelta instance + """ + if isinstance(delta, pendulum.Duration): + return self.subtract( + years=delta.years, + months=delta.months, + weeks=delta.weeks, + days=delta.remaining_days, + ) + + return self.subtract(days=delta.days) + + def __add__(self, other: timedelta) -> Self: + if not isinstance(other, timedelta): + return NotImplemented + + return self._add_timedelta(other) + + @overload # type: ignore[override] # this is only needed because of Python 3.7 + def __sub__(self, __delta: timedelta) -> Self: + ... + + @overload + def __sub__(self, __dt: datetime) -> NoReturn: + ... + + @overload + def __sub__(self, __dt: Self) -> Interval: + ... + + def __sub__(self, other: timedelta | date) -> Self | Interval: + if isinstance(other, timedelta): + return self._subtract_timedelta(other) + + if not isinstance(other, date): + return NotImplemented + + dt = self.__class__(other.year, other.month, other.day) + + return dt.diff(self, False) + + # DIFFERENCES + + def diff(self, dt: date | None = None, abs: bool = True) -> Interval: + """ + Returns the difference between two Date objects as an Interval. + + :param dt: The date to compare to (defaults to today) + :param abs: Whether to return an absolute interval or not + """ + if dt is None: + dt = self.today() + + return Interval(self, Date(dt.year, dt.month, dt.day), absolute=abs) + + def diff_for_humans( + self, + other: date | None = None, + absolute: bool = False, + locale: str | None = None, + ) -> str: + """ + Get the difference in a human readable format in the current locale. + + When comparing a value in the past to default now: + 1 day ago + 5 months ago + + When comparing a value in the future to default now: + 1 day from now + 5 months from now + + When comparing a value in the past to another value: + 1 day before + 5 months before + + When comparing a value in the future to another value: + 1 day after + 5 months after + + :param other: The date to compare to (defaults to today) + :param absolute: removes time difference modifiers ago, after, etc + :param locale: The locale to use for localization + """ + is_now = other is None + + if is_now: + other = self.today() + + diff = self.diff(other) + + return pendulum.format_diff(diff, is_now, absolute, locale) + + # MODIFIERS + + def start_of(self, unit: str) -> Self: + """ + Returns a copy of the instance with the time reset + with the following rules: + + * day: time to 00:00:00 + * week: date to first day of the week and time to 00:00:00 + * month: date to first day of the month and time to 00:00:00 + * year: date to first day of the year and time to 00:00:00 + * decade: date to first day of the decade and time to 00:00:00 + * century: date to first day of century and time to 00:00:00 + + :param unit: The unit to reset to + """ + if unit not in self._MODIFIERS_VALID_UNITS: + raise ValueError(f'Invalid unit "{unit}" for start_of()') + + return cast("Self", getattr(self, f"_start_of_{unit}")()) + + def end_of(self, unit: str) -> Self: + """ + Returns a copy of the instance with the time reset + with the following rules: + + * week: date to last day of the week + * month: date to last day of the month + * year: date to last day of the year + * decade: date to last day of the decade + * century: date to last day of century + + :param unit: The unit to reset to + """ + if unit not in self._MODIFIERS_VALID_UNITS: + raise ValueError(f'Invalid unit "{unit}" for end_of()') + + return cast("Self", getattr(self, f"_end_of_{unit}")()) + + def _start_of_day(self) -> Self: + """ + Compatibility method. + """ + return self + + def _end_of_day(self) -> Self: + """ + Compatibility method + """ + return self + + def _start_of_month(self) -> Self: + """ + Reset the date to the first day of the month. + """ + return self.set(self.year, self.month, 1) + + def _end_of_month(self) -> Self: + """ + Reset the date to the last day of the month. + """ + return self.set(self.year, self.month, self.days_in_month) + + def _start_of_year(self) -> Self: + """ + Reset the date to the first day of the year. + """ + return self.set(self.year, 1, 1) + + def _end_of_year(self) -> Self: + """ + Reset the date to the last day of the year. + """ + return self.set(self.year, 12, 31) + + def _start_of_decade(self) -> Self: + """ + Reset the date to the first day of the decade. + """ + year = self.year - self.year % YEARS_PER_DECADE + + return self.set(year, 1, 1) + + def _end_of_decade(self) -> Self: + """ + Reset the date to the last day of the decade. + """ + year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1 + + return self.set(year, 12, 31) + + def _start_of_century(self) -> Self: + """ + Reset the date to the first day of the century. + """ + year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1 + + return self.set(year, 1, 1) + + def _end_of_century(self) -> Self: + """ + Reset the date to the last day of the century. + """ + year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY + + return self.set(year, 12, 31) + + def _start_of_week(self) -> Self: + """ + Reset the date to the first day of the week. + """ + dt = self + + if self.day_of_week != pendulum._WEEK_STARTS_AT: + dt = self.previous(pendulum._WEEK_STARTS_AT) + + return dt.start_of("day") + + def _end_of_week(self) -> Self: + """ + Reset the date to the last day of the week. + """ + dt = self + + if self.day_of_week != pendulum._WEEK_ENDS_AT: + dt = self.next(pendulum._WEEK_ENDS_AT) + + return dt.end_of("day") + + def next(self, day_of_week: WeekDay | None = None) -> Self: + """ + Modify to the next occurrence of a given day of the week. + If no day_of_week is provided, modify to the next occurrence + of the current day of the week. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + :param day_of_week: The next day of week to reset to. + """ + if day_of_week is None: + day_of_week = self.day_of_week + + if day_of_week < WeekDay.MONDAY or day_of_week > WeekDay.SUNDAY: + raise ValueError("Invalid day of week") + + dt = self.add(days=1) + while dt.day_of_week != day_of_week: + dt = dt.add(days=1) + + return dt + + def previous(self, day_of_week: WeekDay | None = None) -> Self: + """ + Modify to the previous occurrence of a given day of the week. + If no day_of_week is provided, modify to the previous occurrence + of the current day of the week. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + :param day_of_week: The previous day of week to reset to. + """ + if day_of_week is None: + day_of_week = self.day_of_week + + if day_of_week < WeekDay.MONDAY or day_of_week > WeekDay.SUNDAY: + raise ValueError("Invalid day of week") + + dt = self.subtract(days=1) + while dt.day_of_week != day_of_week: + dt = dt.subtract(days=1) + + return dt + + def first_of(self, unit: str, day_of_week: WeekDay | None = None) -> Self: + """ + Returns an instance set to the first occurrence + of a given day of the week in the current unit. + If no day_of_week is provided, modify to the first day of the unit. + Use the supplied consts to indicate the desired day_of_week, + ex. pendulum.MONDAY. + + Supported units are month, quarter and year. + + :param unit: The unit to use + :param day_of_week: The day of week to reset to. + """ + if unit not in ["month", "quarter", "year"]: + raise ValueError(f'Invalid unit "{unit}" for first_of()') + + return cast("Self", getattr(self, f"_first_of_{unit}")(day_of_week)) + + def last_of(self, unit: str, day_of_week: WeekDay | None = None) -> Self: + """ + Returns an instance set to the last occurrence + of a given day of the week in the current unit. + If no day_of_week is provided, modify to the last day of the unit. + Use the supplied consts to indicate the desired day_of_week, + ex. pendulum.MONDAY. + + Supported units are month, quarter and year. + + :param unit: The unit to use + :param day_of_week: The day of week to reset to. + """ + if unit not in ["month", "quarter", "year"]: + raise ValueError(f'Invalid unit "{unit}" for first_of()') + + return cast("Self", getattr(self, f"_last_of_{unit}")(day_of_week)) + + def nth_of(self, unit: str, nth: int, day_of_week: WeekDay) -> Self: + """ + Returns a new instance set to the given occurrence + of a given day of the week in the current unit. + If the calculated occurrence is outside the scope of the current unit, + then raise an error. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + Supported units are month, quarter and year. + + :param unit: The unit to use + :param nth: The occurrence to use + :param day_of_week: The day of week to set to. + """ + if unit not in ["month", "quarter", "year"]: + raise ValueError(f'Invalid unit "{unit}" for first_of()') + + dt = cast("Self", getattr(self, f"_nth_of_{unit}")(nth, day_of_week)) + if not dt: + raise PendulumException( + f"Unable to find occurrence {nth}" + f" of {WeekDay(day_of_week).name.capitalize()} in {unit}" + ) + + return dt + + def _first_of_month(self, day_of_week: WeekDay) -> Self: + """ + Modify to the first occurrence of a given day of the week + in the current month. If no day_of_week is provided, + modify to the first day of the month. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + :param day_of_week: The day of week to set to. + """ + dt = self + + if day_of_week is None: + return dt.set(day=1) + + month = calendar.monthcalendar(dt.year, dt.month) + + calendar_day = day_of_week + + if month[0][calendar_day] > 0: + day_of_month = month[0][calendar_day] + else: + day_of_month = month[1][calendar_day] + + return dt.set(day=day_of_month) + + def _last_of_month(self, day_of_week: WeekDay | None = None) -> Self: + """ + Modify to the last occurrence of a given day of the week + in the current month. If no day_of_week is provided, + modify to the last day of the month. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + :param day_of_week: The day of week to set to. + """ + dt = self + + if day_of_week is None: + return dt.set(day=self.days_in_month) + + month = calendar.monthcalendar(dt.year, dt.month) + + calendar_day = day_of_week + + if month[-1][calendar_day] > 0: + day_of_month = month[-1][calendar_day] + else: + day_of_month = month[-2][calendar_day] + + return dt.set(day=day_of_month) + + def _nth_of_month(self, nth: int, day_of_week: WeekDay) -> Self | None: + """ + Modify to the given occurrence of a given day of the week + in the current month. If the calculated occurrence is outside, + the scope of the current month, then return False and no + modifications are made. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + """ + if nth == 1: + return self.first_of("month", day_of_week) + + dt = self.first_of("month") + check = dt.format("YYYY-MM") + for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + dt = dt.next(day_of_week) + + if dt.format("YYYY-MM") == check: + return self.set(day=dt.day) + + return None + + def _first_of_quarter(self, day_of_week: WeekDay | None = None) -> Self: + """ + Modify to the first occurrence of a given day of the week + in the current quarter. If no day_of_week is provided, + modify to the first day of the quarter. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + """ + return self.set(self.year, self.quarter * 3 - 2, 1).first_of( + "month", day_of_week + ) + + def _last_of_quarter(self, day_of_week: WeekDay | None = None) -> Self: + """ + Modify to the last occurrence of a given day of the week + in the current quarter. If no day_of_week is provided, + modify to the last day of the quarter. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + """ + return self.set(self.year, self.quarter * 3, 1).last_of("month", day_of_week) + + def _nth_of_quarter(self, nth: int, day_of_week: WeekDay) -> Self | None: + """ + Modify to the given occurrence of a given day of the week + in the current quarter. If the calculated occurrence is outside, + the scope of the current quarter, then return False and no + modifications are made. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + """ + if nth == 1: + return self.first_of("quarter", day_of_week) + + dt = self.replace(self.year, self.quarter * 3, 1) + last_month = dt.month + year = dt.year + dt = dt.first_of("quarter") + for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + dt = dt.next(day_of_week) + + if last_month < dt.month or year != dt.year: + return None + + return self.set(self.year, dt.month, dt.day) + + def _first_of_year(self, day_of_week: WeekDay | None = None) -> Self: + """ + Modify to the first occurrence of a given day of the week + in the current year. If no day_of_week is provided, + modify to the first day of the year. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + """ + return self.set(month=1).first_of("month", day_of_week) + + def _last_of_year(self, day_of_week: WeekDay | None = None) -> Self: + """ + Modify to the last occurrence of a given day of the week + in the current year. If no day_of_week is provided, + modify to the last day of the year. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + """ + return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week) + + def _nth_of_year(self, nth: int, day_of_week: WeekDay) -> Self | None: + """ + Modify to the given occurrence of a given day of the week + in the current year. If the calculated occurrence is outside, + the scope of the current year, then return False and no + modifications are made. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + """ + if nth == 1: + return self.first_of("year", day_of_week) + + dt = self.first_of("year") + year = dt.year + for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + dt = dt.next(day_of_week) + + if year != dt.year: + return None + + return self.set(self.year, dt.month, dt.day) + + def average(self, dt: date | None = None) -> Self: + """ + Modify the current instance to the average + of a given instance (default now) and the current instance. + """ + if dt is None: + dt = Date.today() + + return self.add(days=int(self.diff(dt, False).in_days() / 2)) + + # Native methods override + + @classmethod + def today(cls) -> Self: + dt = date.today() + + return cls(dt.year, dt.month, dt.day) + + @classmethod + def fromtimestamp(cls, t: float) -> Self: + dt = super().fromtimestamp(t) + + return cls(dt.year, dt.month, dt.day) + + @classmethod + def fromordinal(cls, n: int) -> Self: + dt = super().fromordinal(n) + + return cls(dt.year, dt.month, dt.day) + + def replace( + self, + year: SupportsIndex | None = None, + month: SupportsIndex | None = None, + day: SupportsIndex | None = None, + ) -> Self: + year = year if year is not None else self.year + month = month if month is not None else self.month + day = day if day is not None else self.day + + return self.__class__(year, month, day) diff --git a/src/pendulum/datetime.py b/src/pendulum/datetime.py new file mode 100644 index 0000000..752a9ac --- /dev/null +++ b/src/pendulum/datetime.py @@ -0,0 +1,1404 @@ +from __future__ import annotations + +import calendar +import datetime +import traceback + +from typing import TYPE_CHECKING +from typing import Any +from typing import Callable +from typing import ClassVar +from typing import Optional +from typing import cast +from typing import overload + +import pendulum + +from pendulum.constants import ATOM +from pendulum.constants import COOKIE +from pendulum.constants import MINUTES_PER_HOUR +from pendulum.constants import MONTHS_PER_YEAR +from pendulum.constants import RFC822 +from pendulum.constants import RFC850 +from pendulum.constants import RFC1036 +from pendulum.constants import RFC1123 +from pendulum.constants import RFC2822 +from pendulum.constants import RSS +from pendulum.constants import SECONDS_PER_DAY +from pendulum.constants import SECONDS_PER_MINUTE +from pendulum.constants import W3C +from pendulum.constants import YEARS_PER_CENTURY +from pendulum.constants import YEARS_PER_DECADE +from pendulum.date import Date +from pendulum.day import WeekDay +from pendulum.exceptions import PendulumException +from pendulum.helpers import add_duration +from pendulum.interval import Interval +from pendulum.time import Time +from pendulum.tz import UTC +from pendulum.tz import local_timezone +from pendulum.tz.timezone import FixedTimezone +from pendulum.tz.timezone import Timezone + + +if TYPE_CHECKING: + from typing_extensions import Literal + from typing_extensions import Self + from typing_extensions import SupportsIndex + + +class DateTime(datetime.datetime, Date): + EPOCH: ClassVar[DateTime] + min: ClassVar[DateTime] + max: ClassVar[DateTime] + + # Formats + + _FORMATS: ClassVar[dict[str, str | Callable[[datetime.datetime], str]]] = { + "atom": ATOM, + "cookie": COOKIE, + "iso8601": lambda dt: dt.isoformat("T"), + "rfc822": RFC822, + "rfc850": RFC850, + "rfc1036": RFC1036, + "rfc1123": RFC1123, + "rfc2822": RFC2822, + "rfc3339": lambda dt: dt.isoformat("T"), + "rss": RSS, + "w3c": W3C, + } + + _MODIFIERS_VALID_UNITS: ClassVar[list[str]] = [ + "second", + "minute", + "hour", + "day", + "week", + "month", + "year", + "decade", + "century", + ] + + _EPOCH: datetime.datetime = datetime.datetime(1970, 1, 1, tzinfo=UTC) + + @classmethod + def create( + cls, + year: SupportsIndex, + month: SupportsIndex, + day: SupportsIndex, + hour: SupportsIndex = 0, + minute: SupportsIndex = 0, + second: SupportsIndex = 0, + microsecond: SupportsIndex = 0, + tz: str | float | Timezone | FixedTimezone | None | datetime.tzinfo = UTC, + fold: int = 1, + raise_on_unknown_times: bool = False, + ) -> Self: + """ + Creates a new DateTime instance from a specific date and time. + """ + if tz is not None: + tz = pendulum._safe_timezone(tz) + + dt = datetime.datetime( + year, month, day, hour, minute, second, microsecond, fold=fold + ) + + if tz is not None: + dt = tz.convert(dt, raise_on_unknown_times=raise_on_unknown_times) + + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=dt.tzinfo, + fold=dt.fold, + ) + + @classmethod + def instance( + cls, + dt: datetime.datetime, + tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = UTC, + ) -> Self: + tz = dt.tzinfo or tz + + if tz is not None: + tz = pendulum._safe_timezone(tz, dt=dt) + + return cls.create( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tz=tz, + fold=dt.fold, + ) + + @overload + @classmethod + def now(cls, tz: datetime.tzinfo | None = None) -> Self: + ... + + @overload + @classmethod + def now(cls, tz: str | Timezone | FixedTimezone | None = None) -> Self: + ... + + @classmethod + def now( + cls, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = None + ) -> Self: + """ + Get a DateTime instance for the current date and time. + """ + if tz is None or tz == "local": + dt = datetime.datetime.now(local_timezone()) + elif tz is UTC or tz == "UTC": + dt = datetime.datetime.now(UTC) + else: + dt = datetime.datetime.now(UTC) + tz = pendulum._safe_timezone(tz) + dt = dt.astimezone(tz) + + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=dt.tzinfo, + fold=dt.fold, + ) + + @classmethod + def utcnow(cls) -> Self: + """ + Get a DateTime instance for the current date and time in UTC. + """ + return cls.now(UTC) + + @classmethod + def today(cls) -> Self: + return cls.now() + + @classmethod + def strptime(cls, time: str, fmt: str) -> Self: + return cls.instance(datetime.datetime.strptime(time, fmt)) + + # Getters/Setters + + def set( + self, + year: int | None = None, + month: int | None = None, + day: int | None = None, + hour: int | None = None, + minute: int | None = None, + second: int | None = None, + microsecond: int | None = None, + tz: str | float | Timezone | FixedTimezone | datetime.tzinfo | None = None, + ) -> Self: + if year is None: + year = self.year + if month is None: + month = self.month + if day is None: + day = self.day + if hour is None: + hour = self.hour + if minute is None: + minute = self.minute + if second is None: + second = self.second + if microsecond is None: + microsecond = self.microsecond + if tz is None: + tz = self.tz + + return self.__class__.create( + year, month, day, hour, minute, second, microsecond, tz=tz, fold=self.fold + ) + + @property + def float_timestamp(self) -> float: + return self.timestamp() + + @property + def int_timestamp(self) -> int: + # Workaround needed to avoid inaccuracy + # for far into the future datetimes + dt = datetime.datetime( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + tzinfo=self.tzinfo, + fold=self.fold, + ) + + delta = dt - self._EPOCH + + return delta.days * SECONDS_PER_DAY + delta.seconds + + @property + def offset(self) -> int | None: + return self.get_offset() + + @property + def offset_hours(self) -> float | None: + offset = self.get_offset() + + if offset is None: + return None + + return offset / SECONDS_PER_MINUTE / MINUTES_PER_HOUR + + @property + def timezone(self) -> Timezone | FixedTimezone | None: + if not isinstance(self.tzinfo, (Timezone, FixedTimezone)): + return None + + return self.tzinfo + + @property + def tz(self) -> Timezone | FixedTimezone | None: + return self.timezone + + @property + def timezone_name(self) -> str | None: + tz = self.timezone + + if tz is None: + return None + + return tz.name + + @property + def age(self) -> int: + return self.date().diff(self.now(self.tz).date(), abs=False).in_years() + + def is_local(self) -> bool: + return self.offset == self.in_timezone(pendulum.local_timezone()).offset + + def is_utc(self) -> bool: + return self.offset == 0 + + def is_dst(self) -> bool: + return self.dst() != datetime.timedelta() + + def get_offset(self) -> int | None: + utcoffset = self.utcoffset() + if utcoffset is None: + return None + + return int(utcoffset.total_seconds()) + + def date(self) -> Date: + return Date(self.year, self.month, self.day) + + def time(self) -> Time: + return Time(self.hour, self.minute, self.second, self.microsecond) + + def naive(self) -> Self: + """ + Return the DateTime without timezone information. + """ + return self.__class__( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + ) + + def on(self, year: int, month: int, day: int) -> Self: + """ + Returns a new instance with the current date set to a different date. + """ + return self.set(year=int(year), month=int(month), day=int(day)) + + def at( + self, hour: int, minute: int = 0, second: int = 0, microsecond: int = 0 + ) -> Self: + """ + Returns a new instance with the current time to a different time. + """ + return self.set( + hour=hour, minute=minute, second=second, microsecond=microsecond + ) + + def in_timezone(self, tz: str | Timezone | FixedTimezone) -> Self: + """ + Set the instance's timezone from a string or object. + """ + tz = pendulum._safe_timezone(tz) + + dt = self + if not self.timezone: + dt = dt.replace(fold=1) + + return tz.convert(dt) + + def in_tz(self, tz: str | Timezone | FixedTimezone) -> Self: + """ + Set the instance's timezone from a string or object. + """ + return self.in_timezone(tz) + + # STRING FORMATTING + + def to_time_string(self) -> str: + """ + Format the instance as time. + """ + return self.format("HH:mm:ss") + + def to_datetime_string(self) -> str: + """ + Format the instance as date and time. + """ + return self.format("YYYY-MM-DD HH:mm:ss") + + def to_day_datetime_string(self) -> str: + """ + Format the instance as day, date and time (in english). + """ + return self.format("ddd, MMM D, YYYY h:mm A", locale="en") + + def to_atom_string(self) -> str: + """ + Format the instance as ATOM. + """ + return self._to_string("atom") + + def to_cookie_string(self) -> str: + """ + Format the instance as COOKIE. + """ + return self._to_string("cookie", locale="en") + + def to_iso8601_string(self) -> str: + """ + Format the instance as ISO 8601. + """ + string = self._to_string("iso8601") + + if self.tz and self.tz.name == "UTC": + string = string.replace("+00:00", "Z") + + return string + + def to_rfc822_string(self) -> str: + """ + Format the instance as RFC 822. + """ + return self._to_string("rfc822") + + def to_rfc850_string(self) -> str: + """ + Format the instance as RFC 850. + """ + return self._to_string("rfc850") + + def to_rfc1036_string(self) -> str: + """ + Format the instance as RFC 1036. + """ + return self._to_string("rfc1036") + + def to_rfc1123_string(self) -> str: + """ + Format the instance as RFC 1123. + """ + return self._to_string("rfc1123") + + def to_rfc2822_string(self) -> str: + """ + Format the instance as RFC 2822. + """ + return self._to_string("rfc2822") + + def to_rfc3339_string(self) -> str: + """ + Format the instance as RFC 3339. + """ + return self._to_string("rfc3339") + + def to_rss_string(self) -> str: + """ + Format the instance as RSS. + """ + return self._to_string("rss") + + def to_w3c_string(self) -> str: + """ + Format the instance as W3C. + """ + return self._to_string("w3c") + + def _to_string(self, fmt: str, locale: str | None = None) -> str: + """ + Format the instance to a common string format. + """ + if fmt not in self._FORMATS: + raise ValueError(f"Format [{fmt}] is not supported") + + fmt_value = self._FORMATS[fmt] + if callable(fmt_value): + return fmt_value(self) + + return self.format(fmt_value, locale=locale) + + def __str__(self) -> str: + return self.isoformat(" ") + + def __repr__(self) -> str: + us = "" + if self.microsecond: + us = f", {self.microsecond}" + + repr_ = "{klass}(" "{year}, {month}, {day}, " "{hour}, {minute}, {second}{us}" + + if self.tzinfo is not None: + repr_ += ", tzinfo={tzinfo}" + + repr_ += ")" + + return repr_.format( + klass=self.__class__.__name__, + year=self.year, + month=self.month, + day=self.day, + hour=self.hour, + minute=self.minute, + second=self.second, + us=us, + tzinfo=repr(self.tzinfo), + ) + + # Comparisons + def closest(self, *dts: datetime.datetime) -> Self: # type: ignore[override] + """ + Get the closest date to the instance. + """ + pdts = [self.instance(x) for x in dts] + + return min((abs(self - dt), dt) for dt in pdts)[1] + + def farthest(self, *dts: datetime.datetime) -> Self: # type: ignore[override] + """ + Get the farthest date from the instance. + """ + pdts = [self.instance(x) for x in dts] + + return max((abs(self - dt), dt) for dt in pdts)[1] + + def is_future(self) -> bool: + """ + Determines if the instance is in the future, ie. greater than now. + """ + return self > self.now(self.timezone) + + def is_past(self) -> bool: + """ + Determines if the instance is in the past, ie. less than now. + """ + return self < self.now(self.timezone) + + def is_long_year(self) -> bool: + """ + Determines if the instance is a long year + + See link `https://en.wikipedia.org/wiki/ISO_8601#Week_dates`_ + """ + return ( + DateTime.create(self.year, 12, 28, 0, 0, 0, tz=self.tz).isocalendar()[1] + == 53 + ) + + def is_same_day(self, dt: datetime.datetime) -> bool: # type: ignore[override] + """ + Checks if the passed in date is the same day + as the instance current day. + """ + dt = self.instance(dt) + + return self.to_date_string() == dt.to_date_string() + + def is_anniversary( # type: ignore[override] + self, dt: datetime.datetime | None = None + ) -> bool: + """ + Check if its the anniversary. + Compares the date/month values of the two dates. + """ + if dt is None: + dt = self.now(self.tz) + + instance = self.instance(dt) + + return (self.month, self.day) == (instance.month, instance.day) + + # ADDITIONS AND SUBSTRACTIONS + + def add( + self, + years: int = 0, + months: int = 0, + weeks: int = 0, + days: int = 0, + hours: int = 0, + minutes: int = 0, + seconds: float = 0, + microseconds: int = 0, + ) -> Self: + """ + Add a duration to the instance. + + If we're adding units of variable length (i.e., years, months), + move forward from current time, otherwise move forward from utc, for accuracy + when moving across DST boundaries. + """ + units_of_variable_length = any([years, months, weeks, days]) + + current_dt = datetime.datetime( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + ) + if not units_of_variable_length: + offset = self.utcoffset() + if offset: + current_dt = current_dt - offset + + dt = add_duration( + current_dt, + years=years, + months=months, + weeks=weeks, + days=days, + hours=hours, + minutes=minutes, + seconds=seconds, + microseconds=microseconds, + ) + + if units_of_variable_length or self.tz is None: + return self.__class__.create( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tz=self.tz, + ) + + dt = datetime.datetime( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=UTC, + ) + + dt = self.tz.convert(dt) + + return self.__class__( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=self.tz, + fold=dt.fold, + ) + + def subtract( + self, + years: int = 0, + months: int = 0, + weeks: int = 0, + days: int = 0, + hours: int = 0, + minutes: int = 0, + seconds: float = 0, + microseconds: int = 0, + ) -> Self: + """ + Remove duration from the instance. + """ + return self.add( + years=-years, + months=-months, + weeks=-weeks, + days=-days, + hours=-hours, + minutes=-minutes, + seconds=-seconds, + microseconds=-microseconds, + ) + + # Adding a final underscore to the method name + # to avoid errors for PyPy which already defines + # a _add_timedelta method + def _add_timedelta_(self, delta: datetime.timedelta) -> Self: + """ + Add timedelta duration to the instance. + """ + if isinstance(delta, pendulum.Interval): + return self.add( + years=delta.years, + months=delta.months, + weeks=delta.weeks, + days=delta.remaining_days, + hours=delta.hours, + minutes=delta.minutes, + seconds=delta.remaining_seconds, + microseconds=delta.microseconds, + ) + elif isinstance(delta, pendulum.Duration): + return self.add(**delta._signature) # type: ignore[attr-defined] + + return self.add(seconds=delta.total_seconds()) + + def _subtract_timedelta(self, delta: datetime.timedelta) -> Self: + """ + Remove timedelta duration from the instance. + """ + if isinstance(delta, pendulum.Duration): + return self.subtract( + years=delta.years, months=delta.months, seconds=delta._total + ) + + return self.subtract(seconds=delta.total_seconds()) + + # DIFFERENCES + + def diff( # type: ignore[override] + self, dt: datetime.datetime | None = None, abs: bool = True + ) -> Interval: + """ + Returns the difference between two DateTime objects represented as an Interval. + """ + if dt is None: + dt = self.now(self.tz) + + return Interval(self, dt, absolute=abs) + + def diff_for_humans( # type: ignore[override] + self, + other: DateTime | None = None, + absolute: bool = False, + locale: str | None = None, + ) -> str: + """ + Get the difference in a human readable format in the current locale. + + When comparing a value in the past to default now: + 1 day ago + 5 months ago + + When comparing a value in the future to default now: + 1 day from now + 5 months from now + + When comparing a value in the past to another value: + 1 day before + 5 months before + + When comparing a value in the future to another value: + 1 day after + 5 months after + """ + is_now = other is None + + if is_now: + other = self.now() + + diff = self.diff(other) + + return pendulum.format_diff(diff, is_now, absolute, locale) + + # Modifiers + def start_of(self, unit: str) -> Self: + """ + Returns a copy of the instance with the time reset + with the following rules: + + * second: microsecond set to 0 + * minute: second and microsecond set to 0 + * hour: minute, second and microsecond set to 0 + * day: time to 00:00:00 + * week: date to first day of the week and time to 00:00:00 + * month: date to first day of the month and time to 00:00:00 + * year: date to first day of the year and time to 00:00:00 + * decade: date to first day of the decade and time to 00:00:00 + * century: date to first day of century and time to 00:00:00 + """ + if unit not in self._MODIFIERS_VALID_UNITS: + raise ValueError(f'Invalid unit "{unit}" for start_of()') + + return cast("Self", getattr(self, f"_start_of_{unit}")()) + + def end_of(self, unit: str) -> Self: + """ + Returns a copy of the instance with the time reset + with the following rules: + + * second: microsecond set to 999999 + * minute: second set to 59 and microsecond set to 999999 + * hour: minute and second set to 59 and microsecond set to 999999 + * day: time to 23:59:59.999999 + * week: date to last day of the week and time to 23:59:59.999999 + * month: date to last day of the month and time to 23:59:59.999999 + * year: date to last day of the year and time to 23:59:59.999999 + * decade: date to last day of the decade and time to 23:59:59.999999 + * century: date to last day of century and time to 23:59:59.999999 + """ + if unit not in self._MODIFIERS_VALID_UNITS: + raise ValueError(f'Invalid unit "{unit}" for end_of()') + + return cast("Self", getattr(self, f"_end_of_{unit}")()) + + def _start_of_second(self) -> Self: + """ + Reset microseconds to 0. + """ + return self.set(microsecond=0) + + def _end_of_second(self) -> Self: + """ + Set microseconds to 999999. + """ + return self.set(microsecond=999999) + + def _start_of_minute(self) -> Self: + """ + Reset seconds and microseconds to 0. + """ + return self.set(second=0, microsecond=0) + + def _end_of_minute(self) -> Self: + """ + Set seconds to 59 and microseconds to 999999. + """ + return self.set(second=59, microsecond=999999) + + def _start_of_hour(self) -> Self: + """ + Reset minutes, seconds and microseconds to 0. + """ + return self.set(minute=0, second=0, microsecond=0) + + def _end_of_hour(self) -> Self: + """ + Set minutes and seconds to 59 and microseconds to 999999. + """ + return self.set(minute=59, second=59, microsecond=999999) + + def _start_of_day(self) -> Self: + """ + Reset the time to 00:00:00. + """ + return self.at(0, 0, 0, 0) + + def _end_of_day(self) -> Self: + """ + Reset the time to 23:59:59.999999. + """ + return self.at(23, 59, 59, 999999) + + def _start_of_month(self) -> Self: + """ + Reset the date to the first day of the month and the time to 00:00:00. + """ + return self.set(self.year, self.month, 1, 0, 0, 0, 0) + + def _end_of_month(self) -> Self: + """ + Reset the date to the last day of the month + and the time to 23:59:59.999999. + """ + return self.set(self.year, self.month, self.days_in_month, 23, 59, 59, 999999) + + def _start_of_year(self) -> Self: + """ + Reset the date to the first day of the year and the time to 00:00:00. + """ + return self.set(self.year, 1, 1, 0, 0, 0, 0) + + def _end_of_year(self) -> Self: + """ + Reset the date to the last day of the year + and the time to 23:59:59.999999. + """ + return self.set(self.year, 12, 31, 23, 59, 59, 999999) + + def _start_of_decade(self) -> Self: + """ + Reset the date to the first day of the decade + and the time to 00:00:00. + """ + year = self.year - self.year % YEARS_PER_DECADE + return self.set(year, 1, 1, 0, 0, 0, 0) + + def _end_of_decade(self) -> Self: + """ + Reset the date to the last day of the decade + and the time to 23:59:59.999999. + """ + year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1 + + return self.set(year, 12, 31, 23, 59, 59, 999999) + + def _start_of_century(self) -> Self: + """ + Reset the date to the first day of the century + and the time to 00:00:00. + """ + year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1 + + return self.set(year, 1, 1, 0, 0, 0, 0) + + def _end_of_century(self) -> Self: + """ + Reset the date to the last day of the century + and the time to 23:59:59.999999. + """ + year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY + + return self.set(year, 12, 31, 23, 59, 59, 999999) + + def _start_of_week(self) -> Self: + """ + Reset the date to the first day of the week + and the time to 00:00:00. + """ + dt = self + + if self.day_of_week != pendulum._WEEK_STARTS_AT: + dt = self.previous(pendulum._WEEK_STARTS_AT) + + return dt.start_of("day") + + def _end_of_week(self) -> Self: + """ + Reset the date to the last day of the week + and the time to 23:59:59. + """ + dt = self + + if self.day_of_week != pendulum._WEEK_ENDS_AT: + dt = self.next(pendulum._WEEK_ENDS_AT) + + return dt.end_of("day") + + def next(self, day_of_week: WeekDay | None = None, keep_time: bool = False) -> Self: + """ + Modify to the next occurrence of a given day of the week. + If no day_of_week is provided, modify to the next occurrence + of the current day of the week. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + """ + if day_of_week is None: + day_of_week = self.day_of_week + + if day_of_week < WeekDay.MONDAY or day_of_week > WeekDay.SUNDAY: + raise ValueError("Invalid day of week") + + dt = self if keep_time else self.start_of("day") + + dt = dt.add(days=1) + while dt.day_of_week != day_of_week: + dt = dt.add(days=1) + + return dt + + def previous( + self, day_of_week: WeekDay | None = None, keep_time: bool = False + ) -> Self: + """ + Modify to the previous occurrence of a given day of the week. + If no day_of_week is provided, modify to the previous occurrence + of the current day of the week. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + """ + if day_of_week is None: + day_of_week = self.day_of_week + + if day_of_week < WeekDay.MONDAY or day_of_week > WeekDay.SUNDAY: + raise ValueError("Invalid day of week") + + dt = self if keep_time else self.start_of("day") + + dt = dt.subtract(days=1) + while dt.day_of_week != day_of_week: + dt = dt.subtract(days=1) + + return dt + + def first_of(self, unit: str, day_of_week: WeekDay | None = None) -> Self: + """ + Returns an instance set to the first occurrence + of a given day of the week in the current unit. + If no day_of_week is provided, modify to the first day of the unit. + Use the supplied consts to indicate the desired day_of_week, + ex. DateTime.MONDAY. + + Supported units are month, quarter and year. + """ + if unit not in ["month", "quarter", "year"]: + raise ValueError(f'Invalid unit "{unit}" for first_of()') + + return cast("Self", getattr(self, f"_first_of_{unit}")(day_of_week)) + + def last_of(self, unit: str, day_of_week: WeekDay | None = None) -> Self: + """ + Returns an instance set to the last occurrence + of a given day of the week in the current unit. + If no day_of_week is provided, modify to the last day of the unit. + Use the supplied consts to indicate the desired day_of_week, + ex. DateTime.MONDAY. + + Supported units are month, quarter and year. + """ + if unit not in ["month", "quarter", "year"]: + raise ValueError(f'Invalid unit "{unit}" for first_of()') + + return cast("Self", getattr(self, f"_last_of_{unit}")(day_of_week)) + + def nth_of(self, unit: str, nth: int, day_of_week: WeekDay) -> Self: + """ + Returns a new instance set to the given occurrence + of a given day of the week in the current unit. + If the calculated occurrence is outside the scope of the current unit, + then raise an error. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + + Supported units are month, quarter and year. + """ + if unit not in ["month", "quarter", "year"]: + raise ValueError(f'Invalid unit "{unit}" for first_of()') + + dt = cast(Optional["Self"], getattr(self, f"_nth_of_{unit}")(nth, day_of_week)) + if not dt: + raise PendulumException( + f"Unable to find occurrence {nth}" + f" of {WeekDay(day_of_week).name.capitalize()} in {unit}" + ) + + return dt + + def _first_of_month(self, day_of_week: WeekDay | None = None) -> Self: + """ + Modify to the first occurrence of a given day of the week + in the current month. If no day_of_week is provided, + modify to the first day of the month. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + """ + dt = self.start_of("day") + + if day_of_week is None: + return dt.set(day=1) + + month = calendar.monthcalendar(dt.year, dt.month) + + calendar_day = day_of_week + + if month[0][calendar_day] > 0: + day_of_month = month[0][calendar_day] + else: + day_of_month = month[1][calendar_day] + + return dt.set(day=day_of_month) + + def _last_of_month(self, day_of_week: WeekDay | None = None) -> Self: + """ + Modify to the last occurrence of a given day of the week + in the current month. If no day_of_week is provided, + modify to the last day of the month. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + """ + dt = self.start_of("day") + + if day_of_week is None: + return dt.set(day=self.days_in_month) + + month = calendar.monthcalendar(dt.year, dt.month) + + calendar_day = day_of_week + + if month[-1][calendar_day] > 0: + day_of_month = month[-1][calendar_day] + else: + day_of_month = month[-2][calendar_day] + + return dt.set(day=day_of_month) + + def _nth_of_month( + self, nth: int, day_of_week: WeekDay | None = None + ) -> Self | None: + """ + Modify to the given occurrence of a given day of the week + in the current month. If the calculated occurrence is outside, + the scope of the current month, then return False and no + modifications are made. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + """ + if nth == 1: + return self.first_of("month", day_of_week) + + dt = self.first_of("month") + check = dt.format("%Y-%M") + for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + dt = dt.next(day_of_week) + + if dt.format("%Y-%M") == check: + return self.set(day=dt.day).start_of("day") + + return None + + def _first_of_quarter(self, day_of_week: WeekDay | None = None) -> Self: + """ + Modify to the first occurrence of a given day of the week + in the current quarter. If no day_of_week is provided, + modify to the first day of the quarter. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + """ + return self.on(self.year, self.quarter * 3 - 2, 1).first_of( + "month", day_of_week + ) + + def _last_of_quarter(self, day_of_week: WeekDay | None = None) -> Self: + """ + Modify to the last occurrence of a given day of the week + in the current quarter. If no day_of_week is provided, + modify to the last day of the quarter. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + """ + return self.on(self.year, self.quarter * 3, 1).last_of("month", day_of_week) + + def _nth_of_quarter( + self, nth: int, day_of_week: WeekDay | None = None + ) -> Self | None: + """ + Modify to the given occurrence of a given day of the week + in the current quarter. If the calculated occurrence is outside, + the scope of the current quarter, then return False and no + modifications are made. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + """ + if nth == 1: + return self.first_of("quarter", day_of_week) + + dt = self.set(day=1, month=self.quarter * 3) + last_month = dt.month + year = dt.year + dt = dt.first_of("quarter") + for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + dt = dt.next(day_of_week) + + if last_month < dt.month or year != dt.year: + return None + + return self.on(self.year, dt.month, dt.day).start_of("day") + + def _first_of_year(self, day_of_week: WeekDay | None = None) -> Self: + """ + Modify to the first occurrence of a given day of the week + in the current year. If no day_of_week is provided, + modify to the first day of the year. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + """ + return self.set(month=1).first_of("month", day_of_week) + + def _last_of_year(self, day_of_week: WeekDay | None = None) -> Self: + """ + Modify to the last occurrence of a given day of the week + in the current year. If no day_of_week is provided, + modify to the last day of the year. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + """ + return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week) + + def _nth_of_year(self, nth: int, day_of_week: WeekDay | None = None) -> Self | None: + """ + Modify to the given occurrence of a given day of the week + in the current year. If the calculated occurrence is outside, + the scope of the current year, then return False and no + modifications are made. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + """ + if nth == 1: + return self.first_of("year", day_of_week) + + dt = self.first_of("year") + year = dt.year + for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + dt = dt.next(day_of_week) + + if year != dt.year: + return None + + return self.on(self.year, dt.month, dt.day).start_of("day") + + def average( # type: ignore[override] + self, dt: datetime.datetime | None = None + ) -> Self: + """ + Modify the current instance to the average + of a given instance (default now) and the current instance. + """ + if dt is None: + dt = self.now(self.tz) + + diff = self.diff(dt, False) + return self.add( + microseconds=(diff.in_seconds() * 1000000 + diff.microseconds) // 2 + ) + + @overload # type: ignore[override] + def __sub__(self, other: datetime.timedelta) -> Self: + ... + + @overload + def __sub__(self, other: DateTime) -> Interval: + ... + + def __sub__(self, other: datetime.datetime | datetime.timedelta) -> Self | Interval: + if isinstance(other, datetime.timedelta): + return self._subtract_timedelta(other) + + if not isinstance(other, datetime.datetime): + return NotImplemented + + if not isinstance(other, self.__class__): + if other.tzinfo is None: + other = pendulum.naive( + other.year, + other.month, + other.day, + other.hour, + other.minute, + other.second, + other.microsecond, + ) + else: + other = self.instance(other) + + return other.diff(self, False) + + def __rsub__(self, other: datetime.datetime) -> Interval: + if not isinstance(other, datetime.datetime): + return NotImplemented + + if not isinstance(other, self.__class__): + if other.tzinfo is None: + other = pendulum.naive( + other.year, + other.month, + other.day, + other.hour, + other.minute, + other.second, + other.microsecond, + ) + else: + other = self.instance(other) + + return self.diff(other, False) + + def __add__(self, other: datetime.timedelta) -> Self: + if not isinstance(other, datetime.timedelta): + return NotImplemented + + caller = traceback.extract_stack(limit=2)[0].name + if caller == "astimezone": + return super().__add__(other) + + return self._add_timedelta_(other) + + def __radd__(self, other: datetime.timedelta) -> Self: + return self.__add__(other) + + # Native methods override + + @classmethod + def fromtimestamp(cls, t: float, tz: datetime.tzinfo | None = None) -> Self: + tzinfo = pendulum._safe_timezone(tz) + + return cls.instance(datetime.datetime.fromtimestamp(t, tz=tzinfo), tz=tzinfo) + + @classmethod + def utcfromtimestamp(cls, t: float) -> Self: + return cls.instance(datetime.datetime.utcfromtimestamp(t), tz=None) + + @classmethod + def fromordinal(cls, n: int) -> Self: + return cls.instance(datetime.datetime.fromordinal(n), tz=None) + + @classmethod + def combine( + cls, + date: datetime.date, + time: datetime.time, + tzinfo: datetime.tzinfo | None = None, + ) -> Self: + return cls.instance(datetime.datetime.combine(date, time), tz=tzinfo) + + def astimezone(self, tz: datetime.tzinfo | None = None) -> Self: + dt = super().astimezone(tz) + + return self.__class__( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + fold=dt.fold, + tzinfo=dt.tzinfo, + ) + + def replace( + self, + year: SupportsIndex | None = None, + month: SupportsIndex | None = None, + day: SupportsIndex | None = None, + hour: SupportsIndex | None = None, + minute: SupportsIndex | None = None, + second: SupportsIndex | None = None, + microsecond: SupportsIndex | None = None, + tzinfo: bool | datetime.tzinfo | Literal[True] | None = True, + fold: int | None = None, + ) -> Self: + if year is None: + year = self.year + if month is None: + month = self.month + if day is None: + day = self.day + if hour is None: + hour = self.hour + if minute is None: + minute = self.minute + if second is None: + second = self.second + if microsecond is None: + microsecond = self.microsecond + if tzinfo is True: + tzinfo = self.tzinfo + if fold is None: + fold = self.fold + + if tzinfo is not None: + tzinfo = pendulum._safe_timezone(tzinfo) + + return self.__class__.create( + year, + month, + day, + hour, + minute, + second, + microsecond, + tz=tzinfo, + fold=fold, + ) + + def __getnewargs__(self) -> tuple[Self]: + return (self,) + + def _getstate( + self, protocol: SupportsIndex = 3 + ) -> tuple[int, int, int, int, int, int, int, datetime.tzinfo | None]: + return ( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + self.tzinfo, + ) + + def __reduce__( + self, + ) -> tuple[ + type[Self], + tuple[int, int, int, int, int, int, int, datetime.tzinfo | None], + ]: + return self.__reduce_ex__(2) + + def __reduce_ex__( + self, protocol: SupportsIndex + ) -> tuple[ + type[Self], + tuple[int, int, int, int, int, int, int, datetime.tzinfo | None], + ]: + return self.__class__, self._getstate(protocol) + + def __deepcopy__(self, _: dict[int, Self]) -> Self: + return self.__class__( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + tzinfo=self.tz, + fold=self.fold, + ) + + def _cmp(self, other: datetime.datetime, **kwargs: Any) -> int: + # Fix for pypy which compares using this method + # which would lead to infinite recursion if we didn't override + dt = datetime.datetime( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + tzinfo=self.tz, + fold=self.fold, + ) + + return 0 if dt == other else 1 if dt > other else -1 + + +DateTime.min = DateTime(1, 1, 1, 0, 0, tzinfo=UTC) +DateTime.max = DateTime(9999, 12, 31, 23, 59, 59, 999999, tzinfo=UTC) +DateTime.EPOCH = DateTime(1970, 1, 1, tzinfo=UTC) diff --git a/src/pendulum/day.py b/src/pendulum/day.py new file mode 100644 index 0000000..7bfffca --- /dev/null +++ b/src/pendulum/day.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from enum import IntEnum + + +class WeekDay(IntEnum): + MONDAY = 0 + TUESDAY = 1 + WEDNESDAY = 2 + THURSDAY = 3 + FRIDAY = 4 + SATURDAY = 5 + SUNDAY = 6 diff --git a/src/pendulum/duration.py b/src/pendulum/duration.py new file mode 100644 index 0000000..a4875fc --- /dev/null +++ b/src/pendulum/duration.py @@ -0,0 +1,533 @@ +from __future__ import annotations + +from datetime import timedelta +from typing import TYPE_CHECKING +from typing import cast +from typing import overload + +import pendulum + +from pendulum.constants import SECONDS_PER_DAY +from pendulum.constants import SECONDS_PER_HOUR +from pendulum.constants import SECONDS_PER_MINUTE +from pendulum.constants import US_PER_SECOND +from pendulum.utils._compat import PYPY + + +if TYPE_CHECKING: + from typing_extensions import Self + + +def _divide_and_round(a: float, b: float) -> int: + """divide a by b and round result to the nearest integer + + When the ratio is exactly half-way between two integers, + the even integer is returned. + """ + # Based on the reference implementation for divmod_near + # in Objects/longobject.c. + q, r = divmod(a, b) + + # The output of divmod() is either a float or an int, + # but we always want it to be an int. + q = int(q) + + # round up if either r / b > 0.5, or r / b == 0.5 and q is odd. + # The expression r / b > 0.5 is equivalent to 2 * r > b if b is + # positive, 2 * r < b if b negative. + r *= 2 + greater_than_half = r > b if b > 0 else r < b + if greater_than_half or r == b and q % 2 == 1: + q += 1 + + return q + + +class Duration(timedelta): + """ + Replacement for the standard timedelta class. + + Provides several improvements over the base class. + """ + + _total: float = 0 + _years: int = 0 + _months: int = 0 + _weeks: int = 0 + _days: int = 0 + _remaining_days: int = 0 + _seconds: int = 0 + _microseconds: int = 0 + + _y = None + _m = None + _w = None + _d = None + _h = None + _i = None + _s = None + _invert = None + + def __new__( + cls, + days: float = 0, + seconds: float = 0, + microseconds: float = 0, + milliseconds: float = 0, + minutes: float = 0, + hours: float = 0, + weeks: float = 0, + years: float = 0, + months: float = 0, + ) -> Self: + if not isinstance(years, int) or not isinstance(months, int): + raise ValueError("Float year and months are not supported") + + self = timedelta.__new__( + cls, + days + years * 365 + months * 30, + seconds, + microseconds, + milliseconds, + minutes, + hours, + weeks, + ) + + # Intuitive normalization + total = self.total_seconds() - (years * 365 + months * 30) * SECONDS_PER_DAY + self._total = total + + m = 1 + if total < 0: + m = -1 + + self._microseconds = round(total % m * 1e6) + self._seconds = abs(int(total)) % SECONDS_PER_DAY * m + + _days = abs(int(total)) // SECONDS_PER_DAY * m + self._days = _days + self._remaining_days = abs(_days) % 7 * m + self._weeks = abs(_days) // 7 * m + self._months = months + self._years = years + + self._signature = { # type: ignore[attr-defined] + "years": years, + "months": months, + "weeks": weeks, + "days": days, + "hours": hours, + "minutes": minutes, + "seconds": seconds, + "microseconds": microseconds + milliseconds * 1000, + } + + return self + + def total_minutes(self) -> float: + return self.total_seconds() / SECONDS_PER_MINUTE + + def total_hours(self) -> float: + return self.total_seconds() / SECONDS_PER_HOUR + + def total_days(self) -> float: + return self.total_seconds() / SECONDS_PER_DAY + + def total_weeks(self) -> float: + return self.total_days() / 7 + + if PYPY: + + def total_seconds(self) -> float: + days = 0 + + if hasattr(self, "_years"): + days += self._years * 365 + + if hasattr(self, "_months"): + days += self._months * 30 + + if hasattr(self, "_remaining_days"): + days += self._weeks * 7 + self._remaining_days + else: + days += self._days + + return ( + (days * SECONDS_PER_DAY + self._seconds) * US_PER_SECOND + + self._microseconds + ) / US_PER_SECOND + + @property + def years(self) -> int: + return self._years + + @property + def months(self) -> int: + return self._months + + @property + def weeks(self) -> int: + return self._weeks + + if PYPY: + + @property + def days(self) -> int: + return self._years * 365 + self._months * 30 + self._days + + @property + def remaining_days(self) -> int: + return self._remaining_days + + @property + def hours(self) -> int: + if self._h is None: + seconds = self._seconds + self._h = 0 + if abs(seconds) >= 3600: + self._h = (abs(seconds) // 3600 % 24) * self._sign(seconds) + + return self._h + + @property + def minutes(self) -> int: + if self._i is None: + seconds = self._seconds + self._i = 0 + if abs(seconds) >= 60: + self._i = (abs(seconds) // 60 % 60) * self._sign(seconds) + + return self._i + + @property + def seconds(self) -> int: + return self._seconds + + @property + def remaining_seconds(self) -> int: + if self._s is None: + self._s = self._seconds + self._s = abs(self._s) % 60 * self._sign(self._s) + + return self._s + + @property + def microseconds(self) -> int: + return self._microseconds + + @property + def invert(self) -> bool: + if self._invert is None: + self._invert = self.total_seconds() < 0 + + return self._invert + + def in_weeks(self) -> int: + return int(self.total_weeks()) + + def in_days(self) -> int: + return int(self.total_days()) + + def in_hours(self) -> int: + return int(self.total_hours()) + + def in_minutes(self) -> int: + return int(self.total_minutes()) + + def in_seconds(self) -> int: + return int(self.total_seconds()) + + def in_words(self, locale: str | None = None, separator: str = " ") -> str: + """ + Get the current interval in words in the current locale. + + Ex: 6 jours 23 heures 58 minutes + + :param locale: The locale to use. Defaults to current locale. + :param separator: The separator to use between each unit + """ + intervals = [ + ("year", self.years), + ("month", self.months), + ("week", self.weeks), + ("day", self.remaining_days), + ("hour", self.hours), + ("minute", self.minutes), + ("second", self.remaining_seconds), + ] + + if locale is None: + locale = pendulum.get_locale() + + loaded_locale = pendulum.locale(locale) + + parts = [] + for interval in intervals: + unit, interval_count = interval + if abs(interval_count) > 0: + translation = loaded_locale.translation( + f"units.{unit}.{loaded_locale.plural(abs(interval_count))}" + ) + parts.append(translation.format(interval_count)) + + if not parts: + count: int | str = 0 + if abs(self.microseconds) > 0: + unit = f"units.second.{loaded_locale.plural(1)}" + count = f"{abs(self.microseconds) / 1e6:.2f}" + else: + unit = f"units.microsecond.{loaded_locale.plural(0)}" + translation = loaded_locale.translation(unit) + parts.append(translation.format(count)) + + return separator.join(parts) + + def _sign(self, value: float) -> int: + if value < 0: + return -1 + + return 1 + + def as_timedelta(self) -> timedelta: + """ + Return the interval as a native timedelta. + """ + return timedelta(seconds=self.total_seconds()) + + def __str__(self) -> str: + return self.in_words() + + def __repr__(self) -> str: + rep = f"{self.__class__.__name__}(" + + if self._years: + rep += f"years={self._years}, " + + if self._months: + rep += f"months={self._months}, " + + if self._weeks: + rep += f"weeks={self._weeks}, " + + if self._days: + rep += f"days={self._remaining_days}, " + + if self.hours: + rep += f"hours={self.hours}, " + + if self.minutes: + rep += f"minutes={self.minutes}, " + + if self.remaining_seconds: + rep += f"seconds={self.remaining_seconds}, " + + if self.microseconds: + rep += f"microseconds={self.microseconds}, " + + rep += ")" + + return rep.replace(", )", ")") + + def __add__(self, other: timedelta) -> Self: + if isinstance(other, timedelta): + return self.__class__(seconds=self.total_seconds() + other.total_seconds()) + + return NotImplemented + + __radd__ = __add__ + + def __sub__(self, other: timedelta) -> Self: + if isinstance(other, timedelta): + return self.__class__(seconds=self.total_seconds() - other.total_seconds()) + + return NotImplemented + + def __neg__(self) -> Self: + return self.__class__( + years=-self._years, + months=-self._months, + weeks=-self._weeks, + days=-self._remaining_days, + seconds=-self._seconds, + microseconds=-self._microseconds, + ) + + def _to_microseconds(self) -> int: + return (self._days * (24 * 3600) + self._seconds) * 1000000 + self._microseconds + + def __mul__(self, other: int | float) -> Self: + if isinstance(other, int): + return self.__class__( + years=self._years * other, + months=self._months * other, + seconds=self._total * other, + ) + + if isinstance(other, float): + usec = self._to_microseconds() + a, b = other.as_integer_ratio() + + return self.__class__(0, 0, _divide_and_round(usec * a, b)) + + return NotImplemented + + __rmul__ = __mul__ + + @overload + def __floordiv__(self, other: timedelta) -> int: + ... + + @overload + def __floordiv__(self, other: int) -> Self: + ... + + def __floordiv__(self, other: int | timedelta) -> int | Duration: + if not isinstance(other, (int, timedelta)): + return NotImplemented + + usec = self._to_microseconds() + if isinstance(other, timedelta): + return cast( + int, usec // other._to_microseconds() # type: ignore[attr-defined] + ) + + if isinstance(other, int): + return self.__class__( + 0, + 0, + usec // other, + years=self._years // other, + months=self._months // other, + ) + + @overload + def __truediv__(self, other: timedelta) -> float: + ... + + @overload + def __truediv__(self, other: float) -> Self: + ... + + def __truediv__(self, other: int | float | timedelta) -> Self | float: + if not isinstance(other, (int, float, timedelta)): + return NotImplemented + + usec = self._to_microseconds() + if isinstance(other, timedelta): + return cast( + float, usec / other._to_microseconds() # type: ignore[attr-defined] + ) + + if isinstance(other, int): + return self.__class__( + 0, + 0, + _divide_and_round(usec, other), + years=_divide_and_round(self._years, other), + months=_divide_and_round(self._months, other), + ) + + if isinstance(other, float): + a, b = other.as_integer_ratio() + + return self.__class__( + 0, + 0, + _divide_and_round(b * usec, a), + years=_divide_and_round(self._years * b, a), + months=_divide_and_round(self._months, other), + ) + + __div__ = __floordiv__ + + def __mod__(self, other: timedelta) -> Self: + if isinstance(other, timedelta): + r = self._to_microseconds() % other._to_microseconds() # type: ignore[attr-defined] # noqa: E501 + + return self.__class__(0, 0, r) + + return NotImplemented + + def __divmod__(self, other: timedelta) -> tuple[int, Duration]: + if isinstance(other, timedelta): + q, r = divmod( + self._to_microseconds(), + other._to_microseconds(), # type: ignore[attr-defined] + ) + + return q, self.__class__(0, 0, r) + + return NotImplemented + + def __deepcopy__(self, _: dict[int, Self]) -> Self: + return self.__class__( + days=self.remaining_days, + seconds=self.remaining_seconds, + microseconds=self.microseconds, + minutes=self.minutes, + hours=self.hours, + years=self.years, + months=self.months, + ) + + +Duration.min = Duration(days=-999999999) +Duration.max = Duration( + days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999 +) +Duration.resolution = Duration(microseconds=1) + + +class AbsoluteDuration(Duration): + """ + Duration that expresses a time difference in absolute values. + """ + + def __new__( + cls, + days: float = 0, + seconds: float = 0, + microseconds: float = 0, + milliseconds: float = 0, + minutes: float = 0, + hours: float = 0, + weeks: float = 0, + years: float = 0, + months: float = 0, + ) -> AbsoluteDuration: + if not isinstance(years, int) or not isinstance(months, int): + raise ValueError("Float year and months are not supported") + + self = timedelta.__new__( + cls, days, seconds, microseconds, milliseconds, minutes, hours, weeks + ) + + # We need to compute the total_seconds() value + # on a native timedelta object + delta = timedelta( + days, seconds, microseconds, milliseconds, minutes, hours, weeks + ) + + # Intuitive normalization + self._total = delta.total_seconds() + total = abs(self._total) + + self._microseconds = round(total % 1 * 1e6) + days, self._seconds = divmod(int(total), SECONDS_PER_DAY) + self._days = abs(days + years * 365 + months * 30) + self._weeks, self._remaining_days = divmod(days, 7) + self._months = abs(months) + self._years = abs(years) + + return self + + def total_seconds(self) -> float: + return abs(self._total) + + @property + def invert(self) -> bool: + if self._invert is None: + self._invert = self._total < 0 + + return self._invert diff --git a/src/pendulum/exceptions.py b/src/pendulum/exceptions.py new file mode 100644 index 0000000..1687211 --- /dev/null +++ b/src/pendulum/exceptions.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from pendulum.parsing.exceptions import ParserError + + +class PendulumException(Exception): + pass + + +__all__ = [ + "ParserError", + "PendulumException", +] diff --git a/src/pendulum/formatting/__init__.py b/src/pendulum/formatting/__init__.py new file mode 100644 index 0000000..0c6e725 --- /dev/null +++ b/src/pendulum/formatting/__init__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from pendulum.formatting.formatter import Formatter + + +__all__ = ["Formatter"] diff --git a/src/pendulum/formatting/difference_formatter.py b/src/pendulum/formatting/difference_formatter.py new file mode 100644 index 0000000..588c072 --- /dev/null +++ b/src/pendulum/formatting/difference_formatter.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +import typing as t + +from pendulum.locales.locale import Locale + + +if t.TYPE_CHECKING: + from pendulum import Duration + + +class DifferenceFormatter: + """ + Handles formatting differences in text. + """ + + def __init__(self, locale: str = "en") -> None: + self._locale = Locale.load(locale) + + def format( + self, + diff: Duration, + is_now: bool = True, + absolute: bool = False, + locale: str | Locale | None = None, + ) -> str: + """ + Formats a difference. + + :param diff: The difference to format + :param is_now: Whether the difference includes now + :param absolute: Whether it's an absolute difference or not + :param locale: The locale to use + """ + locale = self._locale if locale is None else Locale.load(locale) + + if diff.years > 0: + unit = "year" + count = diff.years + + if diff.months > 6: + count += 1 + elif diff.months == 11 and (diff.weeks * 7 + diff.remaining_days) > 15: + unit = "year" + count = 1 + elif diff.months > 0: + unit = "month" + count = diff.months + + if (diff.weeks * 7 + diff.remaining_days) >= 27: + count += 1 + elif diff.weeks > 0: + unit = "week" + count = diff.weeks + + if diff.remaining_days > 3: + count += 1 + elif diff.remaining_days > 0: + unit = "day" + count = diff.remaining_days + + if diff.hours >= 22: + count += 1 + elif diff.hours > 0: + unit = "hour" + count = diff.hours + elif diff.minutes > 0: + unit = "minute" + count = diff.minutes + elif 10 < diff.remaining_seconds <= 59: + unit = "second" + count = diff.remaining_seconds + else: + # We check if the "a few seconds" unit exists + time = locale.get("custom.units.few_second") + if time is not None: + if absolute: + return t.cast(str, time) + + key = "custom" + is_future = diff.invert + if is_now: + if is_future: + key += ".from_now" + else: + key += ".ago" + else: + if is_future: + key += ".after" + else: + key += ".before" + + return t.cast(str, locale.get(key).format(time)) + else: + unit = "second" + count = diff.remaining_seconds + + if count == 0: + count = 1 + + if absolute: + key = f"translations.units.{unit}" + else: + is_future = diff.invert + + if is_now: + # Relative to now, so we can use + # the CLDR data + key = f"translations.relative.{unit}" + + if is_future: + key += ".future" + else: + key += ".past" + else: + # Absolute comparison + # So we have to use the custom locale data + + # Checking for special pluralization rules + key = "custom.units_relative" + if is_future: + key += f".{unit}.future" + else: + key += f".{unit}.past" + + trans = locale.get(key) + if not trans: + # No special rule + key = f"translations.units.{unit}.{locale.plural(count)}" + time = locale.get(key).format(count) + else: + time = trans[locale.plural(count)].format(count) + + key = "custom" + if is_future: + key += ".after" + else: + key += ".before" + + return t.cast(str, locale.get(key).format(time)) + + key += f".{locale.plural(count)}" + + return t.cast(str, locale.get(key).format(count)) diff --git a/src/pendulum/formatting/formatter.py b/src/pendulum/formatting/formatter.py new file mode 100644 index 0000000..ee04063 --- /dev/null +++ b/src/pendulum/formatting/formatter.py @@ -0,0 +1,698 @@ +from __future__ import annotations + +import datetime +import re + +from typing import TYPE_CHECKING +from typing import Any +from typing import Callable +from typing import ClassVar +from typing import Match +from typing import Sequence +from typing import cast + +import pendulum + +from pendulum.locales.locale import Locale + + +if TYPE_CHECKING: + from pendulum import Timezone + +_MATCH_1 = r"\d" +_MATCH_2 = r"\d\d" +_MATCH_3 = r"\d{3}" +_MATCH_4 = r"\d{4}" +_MATCH_6 = r"[+-]?\d{6}" +_MATCH_1_TO_2 = r"\d\d?" +_MATCH_1_TO_2_LEFT_PAD = r"[0-9 ]\d?" +_MATCH_1_TO_3 = r"\d{1,3}" +_MATCH_1_TO_4 = r"\d{1,4}" +_MATCH_1_TO_6 = r"[+-]?\d{1,6}" +_MATCH_3_TO_4 = r"\d{3}\d?" +_MATCH_5_TO_6 = r"\d{5}\d?" +_MATCH_UNSIGNED = r"\d+" +_MATCH_SIGNED = r"[+-]?\d+" +_MATCH_OFFSET = r"[Zz]|[+-]\d\d:?\d\d" +_MATCH_SHORT_OFFSET = r"[Zz]|[+-]\d\d(?::?\d\d)?" +_MATCH_TIMESTAMP = r"[+-]?\d+(\.\d{1,6})?" +_MATCH_WORD = ( + "(?i)[0-9]*" + "['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+" + r"|[\u0600-\u06FF/]+(\s*?[\u0600-\u06FF]+){1,2}" +) +_MATCH_TIMEZONE = "[A-Za-z0-9-+]+(/[A-Za-z0-9-+_]+)?" + + +class Formatter: + _TOKENS: str = ( + r"\[([^\[]*)\]|\\(.)|" + "(" + "Mo|MM?M?M?" + "|Do|DDDo|DD?D?D?|ddd?d?|do?|eo?" + "|E{1,4}" + "|w[o|w]?|W[o|W]?|Qo?" + "|YYYY|YY|Y" + "|gg(ggg?)?|GG(GGG?)?" + "|a|A" + "|hh?|HH?|kk?" + "|mm?|ss?|S{1,9}" + "|x|X" + "|zz?|ZZ?" + "|LTS|LT|LL?L?L?" + ")" + ) + + _FORMAT_RE: re.Pattern[str] = re.compile(_TOKENS) + + _FROM_FORMAT_RE: re.Pattern[str] = re.compile(r"(?<!\\\[)" + _TOKENS + r"(?!\\\])") + + _LOCALIZABLE_TOKENS: ClassVar[ + dict[str, str | Callable[[Locale], Sequence[str]] | None] + ] = { + "Qo": None, + "MMMM": "months.wide", + "MMM": "months.abbreviated", + "Mo": None, + "DDDo": None, + "Do": lambda locale: tuple( + rf"\d+{o}" for o in locale.get("custom.ordinal").values() + ), + "dddd": "days.wide", + "ddd": "days.abbreviated", + "dd": "days.short", + "do": None, + "e": None, + "eo": None, + "Wo": None, + "wo": None, + "A": lambda locale: ( + locale.translation("day_periods.am"), + locale.translation("day_periods.pm"), + ), + "a": lambda locale: ( + locale.translation("day_periods.am").lower(), + locale.translation("day_periods.pm").lower(), + ), + } + + _TOKENS_RULES: ClassVar[dict[str, Callable[[pendulum.DateTime], str]]] = { + # Year + "YYYY": lambda dt: f"{dt.year:d}", + "YY": lambda dt: f"{dt.year:d}"[2:], + "Y": lambda dt: f"{dt.year:d}", + # Quarter + "Q": lambda dt: f"{dt.quarter:d}", + # Month + "MM": lambda dt: f"{dt.month:02d}", + "M": lambda dt: f"{dt.month:d}", + # Day + "DD": lambda dt: f"{dt.day:02d}", + "D": lambda dt: f"{dt.day:d}", + # Day of Year + "DDDD": lambda dt: f"{dt.day_of_year:03d}", + "DDD": lambda dt: f"{dt.day_of_year:d}", + # Day of Week + "d": lambda dt: f"{(dt.day_of_week + 1) % 7:d}", + # Day of ISO Week + "E": lambda dt: f"{dt.isoweekday():d}", + # Hour + "HH": lambda dt: f"{dt.hour:02d}", + "H": lambda dt: f"{dt.hour:d}", + "hh": lambda dt: f"{dt.hour % 12 or 12:02d}", + "h": lambda dt: f"{dt.hour % 12 or 12:d}", + # Minute + "mm": lambda dt: f"{dt.minute:02d}", + "m": lambda dt: f"{dt.minute:d}", + # Second + "ss": lambda dt: f"{dt.second:02d}", + "s": lambda dt: f"{dt.second:d}", + # Fractional second + "S": lambda dt: f"{dt.microsecond // 100000:01d}", + "SS": lambda dt: f"{dt.microsecond // 10000:02d}", + "SSS": lambda dt: f"{dt.microsecond // 1000:03d}", + "SSSS": lambda dt: f"{dt.microsecond // 100:04d}", + "SSSSS": lambda dt: f"{dt.microsecond // 10:05d}", + "SSSSSS": lambda dt: f"{dt.microsecond:06d}", + # Timestamp + "X": lambda dt: f"{dt.int_timestamp:d}", + "x": lambda dt: f"{dt.int_timestamp * 1000 + dt.microsecond // 1000:d}", + # Timezone + "zz": lambda dt: f'{dt.tzname() if dt.tzinfo is not None else ""}', + "z": lambda dt: f'{dt.timezone_name or ""}', + } + + _DATE_FORMATS: ClassVar[dict[str, str]] = { + "LTS": "formats.time.full", + "LT": "formats.time.short", + "L": "formats.date.short", + "LL": "formats.date.long", + "LLL": "formats.datetime.long", + "LLLL": "formats.datetime.full", + } + + _DEFAULT_DATE_FORMATS: ClassVar[dict[str, str]] = { + "LTS": "h:mm:ss A", + "LT": "h:mm A", + "L": "MM/DD/YYYY", + "LL": "MMMM D, YYYY", + "LLL": "MMMM D, YYYY h:mm A", + "LLLL": "dddd, MMMM D, YYYY h:mm A", + } + + _REGEX_TOKENS: ClassVar[dict[str, str | Sequence[str] | None]] = { + "Y": _MATCH_SIGNED, + "YY": (_MATCH_1_TO_2, _MATCH_2), + "YYYY": (_MATCH_1_TO_4, _MATCH_4), + "Q": _MATCH_1, + "Qo": None, + "M": _MATCH_1_TO_2, + "MM": (_MATCH_1_TO_2, _MATCH_2), + "MMM": _MATCH_WORD, + "MMMM": _MATCH_WORD, + "D": _MATCH_1_TO_2, + "DD": (_MATCH_1_TO_2_LEFT_PAD, _MATCH_2), + "DDD": _MATCH_1_TO_3, + "DDDD": _MATCH_3, + "dddd": _MATCH_WORD, + "ddd": _MATCH_WORD, + "dd": _MATCH_WORD, + "d": _MATCH_1, + "e": _MATCH_1, + "E": _MATCH_1, + "Do": None, + "H": _MATCH_1_TO_2, + "HH": (_MATCH_1_TO_2, _MATCH_2), + "h": _MATCH_1_TO_2, + "hh": (_MATCH_1_TO_2, _MATCH_2), + "m": _MATCH_1_TO_2, + "mm": (_MATCH_1_TO_2, _MATCH_2), + "s": _MATCH_1_TO_2, + "ss": (_MATCH_1_TO_2, _MATCH_2), + "S": (_MATCH_1_TO_3, _MATCH_1), + "SS": (_MATCH_1_TO_3, _MATCH_2), + "SSS": (_MATCH_1_TO_3, _MATCH_3), + "SSSS": _MATCH_UNSIGNED, + "SSSSS": _MATCH_UNSIGNED, + "SSSSSS": _MATCH_UNSIGNED, + "x": _MATCH_SIGNED, + "X": _MATCH_TIMESTAMP, + "ZZ": _MATCH_SHORT_OFFSET, + "Z": _MATCH_OFFSET, + "z": _MATCH_TIMEZONE, + } + + _PARSE_TOKENS: ClassVar[dict[str, Callable[[str], Any]]] = { + "YYYY": lambda year: int(year), + "YY": lambda year: int(year), + "Q": lambda quarter: int(quarter), + "MMMM": lambda month: month, + "MMM": lambda month: month, + "MM": lambda month: int(month), + "M": lambda month: int(month), + "DDDD": lambda day: int(day), + "DDD": lambda day: int(day), + "DD": lambda day: int(day), + "D": lambda day: int(day), + "dddd": lambda weekday: weekday, + "ddd": lambda weekday: weekday, + "dd": lambda weekday: weekday, + "d": lambda weekday: int(weekday), + "E": lambda weekday: int(weekday) - 1, + "HH": lambda hour: int(hour), + "H": lambda hour: int(hour), + "hh": lambda hour: int(hour), + "h": lambda hour: int(hour), + "mm": lambda minute: int(minute), + "m": lambda minute: int(minute), + "ss": lambda second: int(second), + "s": lambda second: int(second), + "S": lambda us: int(us) * 100000, + "SS": lambda us: int(us) * 10000, + "SSS": lambda us: int(us) * 1000, + "SSSS": lambda us: int(us) * 100, + "SSSSS": lambda us: int(us) * 10, + "SSSSSS": lambda us: int(us), + "a": lambda meridiem: meridiem, + "X": lambda ts: float(ts), + "x": lambda ts: float(ts) / 1e3, + "ZZ": str, + "Z": str, + "z": str, + } + + def format( + self, dt: pendulum.DateTime, fmt: str, locale: str | Locale | None = None + ) -> str: + """ + Formats a DateTime instance with a given format and locale. + + :param dt: The instance to format + :param fmt: The format to use + :param locale: The locale to use + """ + loaded_locale: Locale = Locale.load(locale or pendulum.get_locale()) + + result = self._FORMAT_RE.sub( + lambda m: m.group(1) + if m.group(1) + else m.group(2) + if m.group(2) + else self._format_token(dt, m.group(3), loaded_locale), + fmt, + ) + + return result + + def _format_token(self, dt: pendulum.DateTime, token: str, locale: Locale) -> str: + """ + Formats a DateTime instance with a given token and locale. + + :param dt: The instance to format + :param token: The token to use + :param locale: The locale to use + """ + if token in self._DATE_FORMATS: + fmt = locale.get(f"custom.date_formats.{token}") + if fmt is None: + fmt = self._DEFAULT_DATE_FORMATS[token] + + return self.format(dt, fmt, locale) + + if token in self._LOCALIZABLE_TOKENS: + return self._format_localizable_token(dt, token, locale) + + if token in self._TOKENS_RULES: + return self._TOKENS_RULES[token](dt) + + # Timezone + if token in ["ZZ", "Z"]: + if dt.tzinfo is None: + return "" + + separator = ":" if token == "Z" else "" + offset = dt.utcoffset() or datetime.timedelta() + minutes = offset.total_seconds() / 60 + + sign = "+" if minutes >= 0 else "-" + + hour, minute = divmod(abs(int(minutes)), 60) + + return f"{sign}{hour:02d}{separator}{minute:02d}" + + return token + + def _format_localizable_token( + self, dt: pendulum.DateTime, token: str, locale: Locale + ) -> str: + """ + Formats a DateTime instance + with a given localizable token and locale. + + :param dt: The instance to format + :param token: The token to use + :param locale: The locale to use + """ + if token == "MMM": + return cast(str, locale.get("translations.months.abbreviated")[dt.month]) + elif token == "MMMM": + return cast(str, locale.get("translations.months.wide")[dt.month]) + elif token == "dd": + return cast(str, locale.get("translations.days.short")[dt.day_of_week]) + elif token == "ddd": + return cast( + str, + locale.get("translations.days.abbreviated")[dt.day_of_week], + ) + elif token == "dddd": + return cast(str, locale.get("translations.days.wide")[dt.day_of_week]) + elif token == "e": + first_day = cast(int, locale.get("translations.week_data.first_day")) + + return str((dt.day_of_week % 7 - first_day) % 7) + elif token == "Do": + return locale.ordinalize(dt.day) + elif token == "do": + return locale.ordinalize((dt.day_of_week + 1) % 7) + elif token == "Mo": + return locale.ordinalize(dt.month) + elif token == "Qo": + return locale.ordinalize(dt.quarter) + elif token == "wo": + return locale.ordinalize(dt.week_of_year) + elif token == "DDDo": + return locale.ordinalize(dt.day_of_year) + elif token == "eo": + first_day = cast(int, locale.get("translations.week_data.first_day")) + + return locale.ordinalize((dt.day_of_week % 7 - first_day) % 7 + 1) + elif token == "A": + key = "translations.day_periods" + if dt.hour >= 12: + key += ".pm" + else: + key += ".am" + + return cast(str, locale.get(key)) + else: + return token + + def parse( + self, + time: str, + fmt: str, + now: pendulum.DateTime, + locale: str | None = None, + ) -> dict[str, Any]: + """ + Parses a time string matching a given format as a tuple. + + :param time: The timestring + :param fmt: The format + :param now: The datetime to use as "now" + :param locale: The locale to use + + :return: The parsed elements + """ + escaped_fmt = re.escape(fmt) + + tokens = self._FROM_FORMAT_RE.findall(escaped_fmt) + if not tokens: + raise ValueError("The given time string does not match the given format") + + if not locale: + locale = pendulum.get_locale() + + loaded_locale: Locale = Locale.load(locale) + + parsed = { + "year": None, + "month": None, + "day": None, + "hour": None, + "minute": None, + "second": None, + "microsecond": None, + "tz": None, + "quarter": None, + "day_of_week": None, + "day_of_year": None, + "meridiem": None, + "timestamp": None, + } + + pattern = self._FROM_FORMAT_RE.sub( + lambda m: self._replace_tokens(m.group(0), loaded_locale), escaped_fmt + ) + + if not re.search("^" + pattern + "$", time): + raise ValueError(f"String does not match format {fmt}") + + def _get_parsed_values(m: Match[str]) -> Any: + return self._get_parsed_values(m, parsed, loaded_locale, now) + + re.sub(pattern, _get_parsed_values, time) + + return self._check_parsed(parsed, now) + + def _check_parsed( + self, parsed: dict[str, Any], now: pendulum.DateTime + ) -> dict[str, Any]: + """ + Checks validity of parsed elements. + + :param parsed: The elements to parse. + + :return: The validated elements. + """ + validated: dict[str, int | Timezone | None] = { + "year": parsed["year"], + "month": parsed["month"], + "day": parsed["day"], + "hour": parsed["hour"], + "minute": parsed["minute"], + "second": parsed["second"], + "microsecond": parsed["microsecond"], + "tz": None, + } + + # If timestamp has been specified + # we use it and don't go any further + if parsed["timestamp"] is not None: + str_us = str(parsed["timestamp"]) + if "." in str_us: + microseconds = int(f'{str_us.split(".")[1].ljust(6, "0")}') + else: + microseconds = 0 + + from pendulum.helpers import local_time + + time = local_time(parsed["timestamp"], 0, microseconds) + validated["year"] = time[0] + validated["month"] = time[1] + validated["day"] = time[2] + validated["hour"] = time[3] + validated["minute"] = time[4] + validated["second"] = time[5] + validated["microsecond"] = time[6] + + return validated + + if parsed["quarter"] is not None: + if validated["year"] is not None: + dt = pendulum.datetime(cast(int, validated["year"]), 1, 1) + else: + dt = now + + dt = dt.start_of("year") + + while dt.quarter != parsed["quarter"]: + dt = dt.add(months=3) + + validated["year"] = dt.year + validated["month"] = dt.month + validated["day"] = dt.day + + if validated["year"] is None: + validated["year"] = now.year + + if parsed["day_of_year"] is not None: + dt = cast( + pendulum.DateTime, + pendulum.parse(f'{validated["year"]}-{parsed["day_of_year"]:>03d}'), + ) + + validated["month"] = dt.month + validated["day"] = dt.day + + if parsed["day_of_week"] is not None: + dt = pendulum.datetime( + cast(int, validated["year"]), + cast(int, validated["month"]) or now.month, + cast(int, validated["day"]) or now.day, + ) + dt = dt.start_of("week").subtract(days=1) + dt = dt.next(parsed["day_of_week"]) + validated["year"] = dt.year + validated["month"] = dt.month + validated["day"] = dt.day + + # Meridiem + if parsed["meridiem"] is not None: + # If the time is greater than 13:00:00 + # This is not valid + if validated["hour"] is None: + raise ValueError("Invalid Date") + + t = ( + validated["hour"], + validated["minute"], + validated["second"], + validated["microsecond"], + ) + if t >= (13, 0, 0, 0): + raise ValueError("Invalid date") + + pm = parsed["meridiem"] == "pm" + validated["hour"] %= 12 # type: ignore[operator] + if pm: + validated["hour"] += 12 # type: ignore[operator] + + if validated["month"] is None: + if parsed["year"] is not None: + validated["month"] = parsed["month"] or 1 + else: + validated["month"] = parsed["month"] or now.month + + if validated["day"] is None: + if parsed["year"] is not None or parsed["month"] is not None: + validated["day"] = parsed["day"] or 1 + else: + validated["day"] = parsed["day"] or now.day + + for part in ["hour", "minute", "second", "microsecond"]: + if validated[part] is None: + validated[part] = 0 + + validated["tz"] = parsed["tz"] + + return validated + + def _get_parsed_values( + self, + m: Match[str], + parsed: dict[str, Any], + locale: Locale, + now: pendulum.DateTime, + ) -> None: + for token, index in m.re.groupindex.items(): + if token in self._LOCALIZABLE_TOKENS: + self._get_parsed_locale_value(token, m.group(index), parsed, locale) + else: + self._get_parsed_value(token, m.group(index), parsed, now) + + def _get_parsed_value( + self, + token: str, + value: str, + parsed: dict[str, Any], + now: pendulum.DateTime, + ) -> None: + parsed_token = self._PARSE_TOKENS[token](value) + + if "Y" in token: + if token == "YY": + if parsed_token <= 68: + parsed_token += 2000 + else: + parsed_token += 1900 + + parsed["year"] = parsed_token + elif token == "Q": + parsed["quarter"] = parsed_token + elif token in ["MM", "M"]: + parsed["month"] = parsed_token + elif token in ["DDDD", "DDD"]: + parsed["day_of_year"] = parsed_token + elif "D" in token: + parsed["day"] = parsed_token + elif "H" in token: + parsed["hour"] = parsed_token + elif token in ["hh", "h"]: + if parsed_token > 12: + raise ValueError("Invalid date") + + parsed["hour"] = parsed_token + elif "m" in token: + parsed["minute"] = parsed_token + elif "s" in token: + parsed["second"] = parsed_token + elif "S" in token: + parsed["microsecond"] = parsed_token + elif token in ["d", "E"]: + parsed["day_of_week"] = parsed_token + elif token in ["X", "x"]: + parsed["timestamp"] = parsed_token + elif token in ["ZZ", "Z"]: + negative = bool(value.startswith("-")) + tz = value[1:] + if ":" not in tz: + if len(tz) == 2: + tz = f"{tz}00" + + off_hour = tz[0:2] + off_minute = tz[2:4] + else: + off_hour, off_minute = tz.split(":") + + offset = ((int(off_hour) * 60) + int(off_minute)) * 60 + + if negative: + offset = -1 * offset + + parsed["tz"] = pendulum.timezone(offset) + elif token == "z": + # Full timezone + if value not in pendulum.timezones(): + raise ValueError("Invalid date") + + parsed["tz"] = pendulum.timezone(value) + + def _get_parsed_locale_value( + self, token: str, value: str, parsed: dict[str, Any], locale: Locale + ) -> None: + if token == "MMMM": + unit = "month" + match = "months.wide" + elif token == "MMM": + unit = "month" + match = "months.abbreviated" + elif token == "Do": + parsed["day"] = int(cast(Match[str], re.match(r"(\d+)", value)).group(1)) + + return + elif token == "dddd": + unit = "day_of_week" + match = "days.wide" + elif token == "ddd": + unit = "day_of_week" + match = "days.abbreviated" + elif token == "dd": + unit = "day_of_week" + match = "days.short" + elif token in ["a", "A"]: + valid_values = [ + locale.translation("day_periods.am"), + locale.translation("day_periods.pm"), + ] + + if token == "a": + value = value.lower() + valid_values = [x.lower() for x in valid_values] + + if value not in valid_values: + raise ValueError("Invalid date") + + parsed["meridiem"] = ["am", "pm"][valid_values.index(value)] + + return + else: + raise ValueError(f'Invalid token "{token}"') + + parsed[unit] = locale.match_translation(match, value) + if value is None: + raise ValueError("Invalid date") + + def _replace_tokens(self, token: str, locale: Locale) -> str: + if token.startswith("[") and token.endswith("]"): + return token[1:-1] + elif token.startswith("\\"): + if len(token) == 2 and token[1] in {"[", "]"}: + return "" + + return token + elif token not in self._REGEX_TOKENS and token not in self._LOCALIZABLE_TOKENS: + raise ValueError(f"Unsupported token: {token}") + + if token in self._LOCALIZABLE_TOKENS: + values = self._LOCALIZABLE_TOKENS[token] + if callable(values): + candidates = values(locale) + else: + candidates = tuple( + locale.translation( + cast(str, self._LOCALIZABLE_TOKENS[token]) + ).values() + ) + else: + candidates = cast(Sequence[str], self._REGEX_TOKENS[token]) + + if not candidates: + raise ValueError(f"Unsupported token: {token}") + + if not isinstance(candidates, tuple): + candidates = (cast(str, candidates),) + + pattern = f'(?P<{token}>{"|".join(candidates)})' + + return pattern diff --git a/src/pendulum/helpers.py b/src/pendulum/helpers.py new file mode 100644 index 0000000..5d5fe8e --- /dev/null +++ b/src/pendulum/helpers.py @@ -0,0 +1,221 @@ +from __future__ import annotations + +import os +import struct + +from datetime import date +from datetime import datetime +from datetime import timedelta +from math import copysign +from typing import TYPE_CHECKING +from typing import TypeVar +from typing import overload + +import pendulum + +from pendulum.constants import DAYS_PER_MONTHS +from pendulum.day import WeekDay +from pendulum.formatting.difference_formatter import DifferenceFormatter +from pendulum.locales.locale import Locale + + +if TYPE_CHECKING: + # Prevent import cycles + from pendulum.duration import Duration + +with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1" + +_DT = TypeVar("_DT", bound=datetime) +_D = TypeVar("_D", bound=date) + +try: + if not with_extensions or struct.calcsize("P") == 4: + raise ImportError() + + from pendulum._pendulum import PreciseDiff + from pendulum._pendulum import days_in_year + from pendulum._pendulum import is_leap + from pendulum._pendulum import is_long_year + from pendulum._pendulum import local_time + from pendulum._pendulum import precise_diff + from pendulum._pendulum import week_day +except ImportError: + from pendulum._helpers import PreciseDiff # type: ignore[assignment] + from pendulum._helpers import days_in_year + from pendulum._helpers import is_leap + from pendulum._helpers import is_long_year + from pendulum._helpers import local_time + from pendulum._helpers import precise_diff # type: ignore[assignment] + from pendulum._helpers import week_day + +difference_formatter = DifferenceFormatter() + + +@overload +def add_duration( + dt: _DT, + years: int = 0, + months: int = 0, + weeks: int = 0, + days: int = 0, + hours: int = 0, + minutes: int = 0, + seconds: float = 0, + microseconds: int = 0, +) -> _DT: + ... + + +@overload +def add_duration( + dt: _D, + years: int = 0, + months: int = 0, + weeks: int = 0, + days: int = 0, +) -> _D: + pass + + +def add_duration( + dt: date | datetime, + years: int = 0, + months: int = 0, + weeks: int = 0, + days: int = 0, + hours: int = 0, + minutes: int = 0, + seconds: float = 0, + microseconds: int = 0, +) -> date | datetime: + """ + Adds a duration to a date/datetime instance. + """ + days += weeks * 7 + + if ( + isinstance(dt, date) + and not isinstance(dt, datetime) + and any([hours, minutes, seconds, microseconds]) + ): + raise RuntimeError("Time elements cannot be added to a date instance.") + + # Normalizing + if abs(microseconds) > 999999: + s = _sign(microseconds) + div, mod = divmod(microseconds * s, 1000000) + microseconds = mod * s + seconds += div * s + + if abs(seconds) > 59: + s = _sign(seconds) + div, mod = divmod(seconds * s, 60) # type: ignore[assignment] + seconds = mod * s + minutes += div * s + + if abs(minutes) > 59: + s = _sign(minutes) + div, mod = divmod(minutes * s, 60) + minutes = mod * s + hours += div * s + + if abs(hours) > 23: + s = _sign(hours) + div, mod = divmod(hours * s, 24) + hours = mod * s + days += div * s + + if abs(months) > 11: + s = _sign(months) + div, mod = divmod(months * s, 12) + months = mod * s + years += div * s + + year = dt.year + years + month = dt.month + + if months: + month += months + if month > 12: + year += 1 + month -= 12 + elif month < 1: + year -= 1 + month += 12 + + day = min(DAYS_PER_MONTHS[int(is_leap(year))][month], dt.day) + + dt = dt.replace(year=year, month=month, day=day) + + return dt + timedelta( + days=days, + hours=hours, + minutes=minutes, + seconds=seconds, + microseconds=microseconds, + ) + + +def format_diff( + diff: Duration, + is_now: bool = True, + absolute: bool = False, + locale: str | None = None, +) -> str: + if locale is None: + locale = get_locale() + + return difference_formatter.format(diff, is_now, absolute, locale) + + +def _sign(x: float) -> int: + return int(copysign(1, x)) + + +# Global helpers + + +def locale(name: str) -> Locale: + return Locale.load(name) + + +def set_locale(name: str) -> None: + locale(name) + + pendulum._LOCALE = name + + +def get_locale() -> str: + return pendulum._LOCALE + + +def week_starts_at(wday: WeekDay) -> None: + if wday < WeekDay.MONDAY or wday > WeekDay.SUNDAY: + raise ValueError("Invalid day of week") + + pendulum._WEEK_STARTS_AT = wday + + +def week_ends_at(wday: WeekDay) -> None: + if wday < WeekDay.MONDAY or wday > WeekDay.SUNDAY: + raise ValueError("Invalid day of week") + + pendulum._WEEK_ENDS_AT = wday + + +__all__ = [ + "PreciseDiff", + "days_in_year", + "is_leap", + "is_long_year", + "local_time", + "precise_diff", + "week_day", + "add_duration", + "format_diff", + "locale", + "set_locale", + "get_locale", + "week_starts_at", + "week_ends_at", +] diff --git a/src/pendulum/interval.py b/src/pendulum/interval.py new file mode 100644 index 0000000..19c91a6 --- /dev/null +++ b/src/pendulum/interval.py @@ -0,0 +1,455 @@ +from __future__ import annotations + +import operator + +from datetime import date +from datetime import datetime +from datetime import timedelta +from typing import TYPE_CHECKING +from typing import Iterator +from typing import Union +from typing import cast +from typing import overload + +import pendulum + +from pendulum.constants import MONTHS_PER_YEAR +from pendulum.duration import Duration +from pendulum.helpers import precise_diff + + +if TYPE_CHECKING: + from typing_extensions import Self + from typing_extensions import SupportsIndex + + from pendulum.helpers import PreciseDiff + from pendulum.locales.locale import Locale + + +class Interval(Duration): + """ + An interval of time between two datetimes. + """ + + @overload + def __new__( + cls, + start: pendulum.DateTime | datetime, + end: pendulum.DateTime | datetime, + absolute: bool = False, + ) -> Self: + ... + + @overload + def __new__( + cls, + start: pendulum.Date | date, + end: pendulum.Date | date, + absolute: bool = False, + ) -> Self: + ... + + def __new__( + cls, + start: pendulum.DateTime | pendulum.Date | datetime | date, + end: pendulum.DateTime | pendulum.Date | datetime | date, + absolute: bool = False, + ) -> Self: + if ( + isinstance(start, datetime) + and not isinstance(end, datetime) + or not isinstance(start, datetime) + and isinstance(end, datetime) + ): + raise ValueError( + "Both start and end of an Interval must have the same type" + ) + + if ( + isinstance(start, datetime) + and isinstance(end, datetime) + and ( + start.tzinfo is None + and end.tzinfo is not None + or start.tzinfo is not None + and end.tzinfo is None + ) + ): + raise TypeError("can't compare offset-naive and offset-aware datetimes") + + if absolute and start > end: + end, start = start, end + + _start = start + _end = end + if isinstance(start, pendulum.DateTime): + _start = datetime( + start.year, + start.month, + start.day, + start.hour, + start.minute, + start.second, + start.microsecond, + tzinfo=start.tzinfo, + fold=start.fold, + ) + elif isinstance(start, pendulum.Date): + _start = date(start.year, start.month, start.day) + + if isinstance(end, pendulum.DateTime): + _end = datetime( + end.year, + end.month, + end.day, + end.hour, + end.minute, + end.second, + end.microsecond, + tzinfo=end.tzinfo, + fold=end.fold, + ) + elif isinstance(end, pendulum.Date): + _end = date(end.year, end.month, end.day) + + # Fixing issues with datetime.__sub__() + # not handling offsets if the tzinfo is the same + if ( + isinstance(_start, datetime) + and isinstance(_end, datetime) + and _start.tzinfo is _end.tzinfo + ): + if _start.tzinfo is not None: + offset = cast(timedelta, cast(datetime, start).utcoffset()) + _start = (_start - offset).replace(tzinfo=None) + + if isinstance(end, datetime) and _end.tzinfo is not None: + offset = cast(timedelta, end.utcoffset()) + _end = (_end - offset).replace(tzinfo=None) + + delta: timedelta = _end - _start # type: ignore[operator] + + return super().__new__(cls, seconds=delta.total_seconds()) + + def __init__( + self, + start: pendulum.DateTime | pendulum.Date | datetime | date, + end: pendulum.DateTime | pendulum.Date | datetime | date, + absolute: bool = False, + ) -> None: + super().__init__() + + _start: pendulum.DateTime | pendulum.Date | datetime | date + if not isinstance(start, pendulum.Date): + if isinstance(start, datetime): + start = pendulum.instance(start) + else: + start = pendulum.date(start.year, start.month, start.day) + + _start = start + else: + if isinstance(start, pendulum.DateTime): + _start = datetime( + start.year, + start.month, + start.day, + start.hour, + start.minute, + start.second, + start.microsecond, + tzinfo=start.tzinfo, + ) + else: + _start = date(start.year, start.month, start.day) + + _end: pendulum.DateTime | pendulum.Date | datetime | date + if not isinstance(end, pendulum.Date): + if isinstance(end, datetime): + end = pendulum.instance(end) + else: + end = pendulum.date(end.year, end.month, end.day) + + _end = end + else: + if isinstance(end, pendulum.DateTime): + _end = datetime( + end.year, + end.month, + end.day, + end.hour, + end.minute, + end.second, + end.microsecond, + tzinfo=end.tzinfo, + ) + else: + _end = date(end.year, end.month, end.day) + + self._invert = False + if start > end: + self._invert = True + + if absolute: + end, start = start, end + _end, _start = _start, _end + + self._absolute = absolute + self._start: pendulum.DateTime | pendulum.Date = start + self._end: pendulum.DateTime | pendulum.Date = end + self._delta: PreciseDiff = precise_diff(_start, _end) + + @property + def years(self) -> int: + return self._delta.years + + @property + def months(self) -> int: + return self._delta.months + + @property + def weeks(self) -> int: + return abs(self._delta.days) // 7 * self._sign(self._delta.days) + + @property + def days(self) -> int: + return self._days + + @property + def remaining_days(self) -> int: + return abs(self._delta.days) % 7 * self._sign(self._days) + + @property + def hours(self) -> int: + return self._delta.hours + + @property + def minutes(self) -> int: + return self._delta.minutes + + @property + def start(self) -> pendulum.DateTime | pendulum.Date | datetime | date: + return self._start + + @property + def end(self) -> pendulum.DateTime | pendulum.Date | datetime | date: + return self._end + + def in_years(self) -> int: + """ + Gives the duration of the Interval in full years. + """ + return self.years + + def in_months(self) -> int: + """ + Gives the duration of the Interval in full months. + """ + return self.years * MONTHS_PER_YEAR + self.months + + def in_weeks(self) -> int: + days = self.in_days() + sign = 1 + + if days < 0: + sign = -1 + + return sign * (abs(days) // 7) + + def in_days(self) -> int: + return self._delta.total_days + + def in_words(self, locale: str | None = None, separator: str = " ") -> str: + """ + Get the current interval in words in the current locale. + + Ex: 6 jours 23 heures 58 minutes + + :param locale: The locale to use. Defaults to current locale. + :param separator: The separator to use between each unit + """ + from pendulum.locales.locale import Locale + + intervals = [ + ("year", self.years), + ("month", self.months), + ("week", self.weeks), + ("day", self.remaining_days), + ("hour", self.hours), + ("minute", self.minutes), + ("second", self.remaining_seconds), + ] + loaded_locale: Locale = Locale.load(locale or pendulum.get_locale()) + parts = [] + for interval in intervals: + unit, interval_count = interval + if abs(interval_count) > 0: + translation = loaded_locale.translation( + f"units.{unit}.{loaded_locale.plural(abs(interval_count))}" + ) + parts.append(translation.format(interval_count)) + + if not parts: + count: str | int = 0 + if abs(self.microseconds) > 0: + unit = f"units.second.{loaded_locale.plural(1)}" + count = f"{abs(self.microseconds) / 1e6:.2f}" + else: + unit = f"units.microsecond.{loaded_locale.plural(0)}" + + translation = loaded_locale.translation(unit) + parts.append(translation.format(count)) + + return separator.join(parts) + + def range( + self, unit: str, amount: int = 1 + ) -> Iterator[pendulum.DateTime | pendulum.Date]: + method = "add" + op = operator.le + if not self._absolute and self.invert: + method = "subtract" + op = operator.ge + + start, end = self.start, self.end + + i = amount + while op(start, end): + yield cast(Union[pendulum.DateTime, pendulum.Date], start) + + start = getattr(self.start, method)(**{unit: i}) + + i += amount + + def as_duration(self) -> Duration: + """ + Return the Interval as a Duration. + """ + return Duration(seconds=self.total_seconds()) + + def __iter__(self) -> Iterator[pendulum.DateTime | pendulum.Date]: + return self.range("days") + + def __contains__( + self, item: datetime | date | pendulum.DateTime | pendulum.Date + ) -> bool: + return self.start <= item <= self.end + + def __add__(self, other: timedelta) -> Duration: # type: ignore[override] + return self.as_duration().__add__(other) + + __radd__ = __add__ # type: ignore[assignment] + + def __sub__(self, other: timedelta) -> Duration: # type: ignore[override] + return self.as_duration().__sub__(other) + + def __neg__(self) -> Self: + return self.__class__(self.end, self.start, self._absolute) + + def __mul__(self, other: int | float) -> Duration: # type: ignore[override] + return self.as_duration().__mul__(other) + + __rmul__ = __mul__ # type: ignore[assignment] + + @overload # type: ignore[override] + def __floordiv__(self, other: timedelta) -> int: + ... + + @overload + def __floordiv__(self, other: int) -> Duration: + ... + + def __floordiv__(self, other: int | timedelta) -> int | Duration: + return self.as_duration().__floordiv__(other) + + __div__ = __floordiv__ # type: ignore[assignment] + + @overload # type: ignore[override] + def __truediv__(self, other: timedelta) -> float: + ... + + @overload + def __truediv__(self, other: float) -> Duration: + ... + + def __truediv__(self, other: float | timedelta) -> Duration | float: + return self.as_duration().__truediv__(other) + + def __mod__(self, other: timedelta) -> Duration: # type: ignore[override] + return self.as_duration().__mod__(other) + + def __divmod__(self, other: timedelta) -> tuple[int, Duration]: + return self.as_duration().__divmod__(other) + + def __abs__(self) -> Self: + return self.__class__(self.start, self.end, absolute=True) + + def __repr__(self) -> str: + return f"<Interval [{self._start} -> {self._end}]>" + + def __str__(self) -> str: + return self.__repr__() + + def _cmp(self, other: timedelta) -> int: + # Only needed for PyPy + assert isinstance(other, timedelta) + + if isinstance(other, Interval): + other = other.as_timedelta() + + td = self.as_timedelta() + + return 0 if td == other else 1 if td > other else -1 + + def _getstate( + self, protocol: SupportsIndex = 3 + ) -> tuple[ + pendulum.DateTime | pendulum.Date | datetime | date, + pendulum.DateTime | pendulum.Date | datetime | date, + bool, + ]: + start, end = self.start, self.end + + if self._invert and self._absolute: + end, start = start, end + + return start, end, self._absolute + + def __reduce__( + self, + ) -> tuple[ + type[Self], + tuple[ + pendulum.DateTime | pendulum.Date | datetime | date, + pendulum.DateTime | pendulum.Date | datetime | date, + bool, + ], + ]: + return self.__reduce_ex__(2) + + def __reduce_ex__( + self, protocol: SupportsIndex + ) -> tuple[ + type[Self], + tuple[ + pendulum.DateTime | pendulum.Date | datetime | date, + pendulum.DateTime | pendulum.Date | datetime | date, + bool, + ], + ]: + return self.__class__, self._getstate(protocol) + + def __hash__(self) -> int: + return hash((self.start, self.end, self._absolute)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Interval): + return (self.start, self.end, self._absolute) == ( + other.start, + other.end, + other._absolute, + ) + else: + return self.as_duration() == other + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) diff --git a/src/pendulum/locales/__init__.py b/src/pendulum/locales/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/__init__.py diff --git a/src/pendulum/locales/cs/__init__.py b/src/pendulum/locales/cs/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/cs/__init__.py diff --git a/src/pendulum/locales/cs/custom.py b/src/pendulum/locales/cs/custom.py new file mode 100644 index 0000000..c909f32 --- /dev/null +++ b/src/pendulum/locales/cs/custom.py @@ -0,0 +1,25 @@ +""" +cs custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "pár vteřin"}, + # Relative time + "ago": "{} zpět", + "from_now": "za {}", + "after": "{0} po", + "before": "{0} zpět", + # Ordinals + "ordinal": {"one": ".", "two": ".", "few": ".", "other": "."}, + # Date formats + "date_formats": { + "LTS": "h:mm:ss", + "LT": "h:mm", + "L": "DD. M. YYYY", + "LL": "D. MMMM, YYYY", + "LLL": "D. MMMM, YYYY h:mm", + "LLLL": "dddd, D. MMMM, YYYY h:mm", + }, +} diff --git a/src/pendulum/locales/cs/locale.py b/src/pendulum/locales/cs/locale.py new file mode 100644 index 0000000..b44d0f7 --- /dev/null +++ b/src/pendulum/locales/cs/locale.py @@ -0,0 +1,274 @@ +from __future__ import annotations + +from pendulum.locales.cs.custom import translations as custom_translations + + +""" +cs locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "few" + if ((n == n and (n >= 2 and n <= 4)) and (0 == 0 and (0 == 0))) + else "many" + if (not (0 == 0 and (0 == 0))) + else "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "po", + 1: "út", + 2: "st", + 3: "čt", + 4: "pá", + 5: "so", + 6: "ne", + }, + "narrow": { + 0: "P", + 1: "Ú", + 2: "S", + 3: "Č", + 4: "P", + 5: "S", + 6: "N", + }, + "short": { + 0: "po", + 1: "út", + 2: "st", + 3: "čt", + 4: "pá", + 5: "so", + 6: "ne", + }, + "wide": { + 0: "pondělí", + 1: "úterý", + 2: "středa", + 3: "čtvrtek", + 4: "pátek", + 5: "sobota", + 6: "neděle", + }, + }, + "months": { + "abbreviated": { + 1: "led", + 2: "úno", + 3: "bře", + 4: "dub", + 5: "kvě", + 6: "čvn", + 7: "čvc", + 8: "srp", + 9: "zář", + 10: "říj", + 11: "lis", + 12: "pro", + }, + "narrow": { + 1: "1", + 2: "2", + 3: "3", + 4: "4", + 5: "5", + 6: "6", + 7: "7", + 8: "8", + 9: "9", + 10: "10", + 11: "11", + 12: "12", + }, + "wide": { + 1: "ledna", + 2: "února", + 3: "března", + 4: "dubna", + 5: "května", + 6: "června", + 7: "července", + 8: "srpna", + 9: "září", + 10: "října", + 11: "listopadu", + 12: "prosince", + }, + }, + "units": { + "year": { + "one": "{0} rok", + "few": "{0} roky", + "many": "{0} roku", + "other": "{0} let", + }, + "month": { + "one": "{0} měsíc", + "few": "{0} měsíce", + "many": "{0} měsíce", + "other": "{0} měsíců", + }, + "week": { + "one": "{0} týden", + "few": "{0} týdny", + "many": "{0} týdne", + "other": "{0} týdnů", + }, + "day": { + "one": "{0} den", + "few": "{0} dny", + "many": "{0} dne", + "other": "{0} dní", + }, + "hour": { + "one": "{0} hodina", + "few": "{0} hodiny", + "many": "{0} hodiny", + "other": "{0} hodin", + }, + "minute": { + "one": "{0} minuta", + "few": "{0} minuty", + "many": "{0} minuty", + "other": "{0} minut", + }, + "second": { + "one": "{0} sekunda", + "few": "{0} sekundy", + "many": "{0} sekundy", + "other": "{0} sekund", + }, + "microsecond": { + "one": "{0} mikrosekunda", + "few": "{0} mikrosekundy", + "many": "{0} mikrosekundy", + "other": "{0} mikrosekund", + }, + }, + "relative": { + "year": { + "future": { + "other": "za {0} let", + "one": "za {0} rok", + "few": "za {0} roky", + "many": "za {0} roku", + }, + "past": { + "other": "před {0} lety", + "one": "před {0} rokem", + "few": "před {0} lety", + "many": "před {0} roku", + }, + }, + "month": { + "future": { + "other": "za {0} měsíců", + "one": "za {0} měsíc", + "few": "za {0} měsíce", + "many": "za {0} měsíce", + }, + "past": { + "other": "před {0} měsíci", + "one": "před {0} měsícem", + "few": "před {0} měsíci", + "many": "před {0} měsíce", + }, + }, + "week": { + "future": { + "other": "za {0} týdnů", + "one": "za {0} týden", + "few": "za {0} týdny", + "many": "za {0} týdne", + }, + "past": { + "other": "před {0} týdny", + "one": "před {0} týdnem", + "few": "před {0} týdny", + "many": "před {0} týdne", + }, + }, + "day": { + "future": { + "other": "za {0} dní", + "one": "za {0} den", + "few": "za {0} dny", + "many": "za {0} dne", + }, + "past": { + "other": "před {0} dny", + "one": "před {0} dnem", + "few": "před {0} dny", + "many": "před {0} dne", + }, + }, + "hour": { + "future": { + "other": "za {0} hodin", + "one": "za {0} hodinu", + "few": "za {0} hodiny", + "many": "za {0} hodiny", + }, + "past": { + "other": "před {0} hodinami", + "one": "před {0} hodinou", + "few": "před {0} hodinami", + "many": "před {0} hodiny", + }, + }, + "minute": { + "future": { + "other": "za {0} minut", + "one": "za {0} minutu", + "few": "za {0} minuty", + "many": "za {0} minuty", + }, + "past": { + "other": "před {0} minutami", + "one": "před {0} minutou", + "few": "před {0} minutami", + "many": "před {0} minuty", + }, + }, + "second": { + "future": { + "other": "za {0} sekund", + "one": "za {0} sekundu", + "few": "za {0} sekundy", + "many": "za {0} sekundy", + }, + "past": { + "other": "před {0} sekundami", + "one": "před {0} sekundou", + "few": "před {0} sekundami", + "many": "před {0} sekundy", + }, + }, + }, + "day_periods": { + "midnight": "půlnoc", + "am": "dop.", + "noon": "poledne", + "pm": "odp.", + "morning1": "ráno", + "morning2": "dopoledne", + "afternoon1": "odpoledne", + "evening1": "večer", + "night1": "v noci", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/da/__init__.py b/src/pendulum/locales/da/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/da/__init__.py diff --git a/src/pendulum/locales/da/custom.py b/src/pendulum/locales/da/custom.py new file mode 100644 index 0000000..57e68f4 --- /dev/null +++ b/src/pendulum/locales/da/custom.py @@ -0,0 +1,20 @@ +""" +da custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "after": "{0} efter", + "before": "{0} før", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd [d.] D. MMMM YYYY HH:mm", + "LLL": "D. MMMM YYYY HH:mm", + "LL": "D. MMMM YYYY", + "L": "DD/MM/YYYY", + }, +} diff --git a/src/pendulum/locales/da/locale.py b/src/pendulum/locales/da/locale.py new file mode 100644 index 0000000..2385de5 --- /dev/null +++ b/src/pendulum/locales/da/locale.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +from pendulum.locales.da.custom import translations as custom_translations + + +""" +da locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ( + (n == n and (n == 1)) + or ((not (0 == 0 and (0 == 0))) and (n == n and ((n == 0) or (n == 1)))) + ) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "man.", + 1: "tir.", + 2: "ons.", + 3: "tor.", + 4: "fre.", + 5: "lør.", + 6: "søn.", + }, + "narrow": {0: "M", 1: "T", 2: "O", 3: "T", 4: "F", 5: "L", 6: "S"}, + "short": {0: "ma", 1: "ti", 2: "on", 3: "to", 4: "fr", 5: "lø", 6: "sø"}, + "wide": { + 0: "mandag", + 1: "tirsdag", + 2: "onsdag", + 3: "torsdag", + 4: "fredag", + 5: "lørdag", + 6: "søndag", + }, + }, + "months": { + "abbreviated": { + 1: "jan.", + 2: "feb.", + 3: "mar.", + 4: "apr.", + 5: "maj", + 6: "jun.", + 7: "jul.", + 8: "aug.", + 9: "sep.", + 10: "okt.", + 11: "nov.", + 12: "dec.", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "januar", + 2: "februar", + 3: "marts", + 4: "april", + 5: "maj", + 6: "juni", + 7: "juli", + 8: "august", + 9: "september", + 10: "oktober", + 11: "november", + 12: "december", + }, + }, + "units": { + "year": {"one": "{0} år", "other": "{0} år"}, + "month": {"one": "{0} måned", "other": "{0} måneder"}, + "week": {"one": "{0} uge", "other": "{0} uger"}, + "day": {"one": "{0} dag", "other": "{0} dage"}, + "hour": {"one": "{0} time", "other": "{0} timer"}, + "minute": {"one": "{0} minut", "other": "{0} minutter"}, + "second": {"one": "{0} sekund", "other": "{0} sekunder"}, + "microsecond": {"one": "{0} mikrosekund", "other": "{0} mikrosekunder"}, + }, + "relative": { + "year": { + "future": {"other": "om {0} år", "one": "om {0} år"}, + "past": {"other": "for {0} år siden", "one": "for {0} år siden"}, + }, + "month": { + "future": {"other": "om {0} måneder", "one": "om {0} måned"}, + "past": { + "other": "for {0} måneder siden", + "one": "for {0} måned siden", + }, + }, + "week": { + "future": {"other": "om {0} uger", "one": "om {0} uge"}, + "past": {"other": "for {0} uger siden", "one": "for {0} uge siden"}, + }, + "day": { + "future": {"other": "om {0} dage", "one": "om {0} dag"}, + "past": {"other": "for {0} dage siden", "one": "for {0} dag siden"}, + }, + "hour": { + "future": {"other": "om {0} timer", "one": "om {0} time"}, + "past": {"other": "for {0} timer siden", "one": "for {0} time siden"}, + }, + "minute": { + "future": {"other": "om {0} minutter", "one": "om {0} minut"}, + "past": { + "other": "for {0} minutter siden", + "one": "for {0} minut siden", + }, + }, + "second": { + "future": {"other": "om {0} sekunder", "one": "om {0} sekund"}, + "past": { + "other": "for {0} sekunder siden", + "one": "for {0} sekund siden", + }, + }, + }, + "day_periods": { + "midnight": "midnat", + "am": "AM", + "pm": "PM", + "morning1": "om morgenen", + "morning2": "om formiddagen", + "afternoon1": "om eftermiddagen", + "evening1": "om aftenen", + "night1": "om natten", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/de/__init__.py b/src/pendulum/locales/de/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/de/__init__.py diff --git a/src/pendulum/locales/de/custom.py b/src/pendulum/locales/de/custom.py new file mode 100644 index 0000000..8ef06cc --- /dev/null +++ b/src/pendulum/locales/de/custom.py @@ -0,0 +1,38 @@ +""" +de custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "after": "{0} später", + "before": "{0} zuvor", + "units_relative": { + "year": { + "future": {"one": "{0} Jahr", "other": "{0} Jahren"}, + "past": {"one": "{0} Jahr", "other": "{0} Jahren"}, + }, + "month": { + "future": {"one": "{0} Monat", "other": "{0} Monaten"}, + "past": {"one": "{0} Monat", "other": "{0} Monaten"}, + }, + "week": { + "future": {"one": "{0} Woche", "other": "{0} Wochen"}, + "past": {"one": "{0} Woche", "other": "{0} Wochen"}, + }, + "day": { + "future": {"one": "{0} Tag", "other": "{0} Tagen"}, + "past": {"one": "{0} Tag", "other": "{0} Tagen"}, + }, + }, + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd, D. MMMM YYYY HH:mm", + "LLL": "D. MMMM YYYY HH:mm", + "LL": "D. MMMM YYYY", + "L": "DD.MM.YYYY", + }, +} diff --git a/src/pendulum/locales/de/locale.py b/src/pendulum/locales/de/locale.py new file mode 100644 index 0000000..7781c3f --- /dev/null +++ b/src/pendulum/locales/de/locale.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +from pendulum.locales.de.custom import translations as custom_translations + + +""" +de locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "Mo.", + 1: "Di.", + 2: "Mi.", + 3: "Do.", + 4: "Fr.", + 5: "Sa.", + 6: "So.", + }, + "narrow": {0: "M", 1: "D", 2: "M", 3: "D", 4: "F", 5: "S", 6: "S"}, + "short": { + 0: "Mo.", + 1: "Di.", + 2: "Mi.", + 3: "Do.", + 4: "Fr.", + 5: "Sa.", + 6: "So.", + }, + "wide": { + 0: "Montag", + 1: "Dienstag", + 2: "Mittwoch", + 3: "Donnerstag", + 4: "Freitag", + 5: "Samstag", + 6: "Sonntag", + }, + }, + "months": { + "abbreviated": { + 1: "Jan.", + 2: "Feb.", + 3: "März", + 4: "Apr.", + 5: "Mai", + 6: "Juni", + 7: "Juli", + 8: "Aug.", + 9: "Sep.", + 10: "Okt.", + 11: "Nov.", + 12: "Dez.", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "Januar", + 2: "Februar", + 3: "März", + 4: "April", + 5: "Mai", + 6: "Juni", + 7: "Juli", + 8: "August", + 9: "September", + 10: "Oktober", + 11: "November", + 12: "Dezember", + }, + }, + "units": { + "year": {"one": "{0} Jahr", "other": "{0} Jahre"}, + "month": {"one": "{0} Monat", "other": "{0} Monate"}, + "week": {"one": "{0} Woche", "other": "{0} Wochen"}, + "day": {"one": "{0} Tag", "other": "{0} Tage"}, + "hour": {"one": "{0} Stunde", "other": "{0} Stunden"}, + "minute": {"one": "{0} Minute", "other": "{0} Minuten"}, + "second": {"one": "{0} Sekunde", "other": "{0} Sekunden"}, + "microsecond": {"one": "{0} Mikrosekunde", "other": "{0} Mikrosekunden"}, + }, + "relative": { + "year": { + "future": {"other": "in {0} Jahren", "one": "in {0} Jahr"}, + "past": {"other": "vor {0} Jahren", "one": "vor {0} Jahr"}, + }, + "month": { + "future": {"other": "in {0} Monaten", "one": "in {0} Monat"}, + "past": {"other": "vor {0} Monaten", "one": "vor {0} Monat"}, + }, + "week": { + "future": {"other": "in {0} Wochen", "one": "in {0} Woche"}, + "past": {"other": "vor {0} Wochen", "one": "vor {0} Woche"}, + }, + "day": { + "future": {"other": "in {0} Tagen", "one": "in {0} Tag"}, + "past": {"other": "vor {0} Tagen", "one": "vor {0} Tag"}, + }, + "hour": { + "future": {"other": "in {0} Stunden", "one": "in {0} Stunde"}, + "past": {"other": "vor {0} Stunden", "one": "vor {0} Stunde"}, + }, + "minute": { + "future": {"other": "in {0} Minuten", "one": "in {0} Minute"}, + "past": {"other": "vor {0} Minuten", "one": "vor {0} Minute"}, + }, + "second": { + "future": {"other": "in {0} Sekunden", "one": "in {0} Sekunde"}, + "past": {"other": "vor {0} Sekunden", "one": "vor {0} Sekunde"}, + }, + }, + "day_periods": { + "midnight": "Mitternacht", + "am": "vorm.", + "pm": "nachm.", + "morning1": "morgens", + "morning2": "vormittags", + "afternoon1": "mittags", + "afternoon2": "nachmittags", + "evening1": "abends", + "night1": "nachts", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/en/__init__.py b/src/pendulum/locales/en/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/en/__init__.py diff --git a/src/pendulum/locales/en/custom.py b/src/pendulum/locales/en/custom.py new file mode 100644 index 0000000..65cf467 --- /dev/null +++ b/src/pendulum/locales/en/custom.py @@ -0,0 +1,25 @@ +""" +en custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "a few seconds"}, + # Relative time + "ago": "{} ago", + "from_now": "in {}", + "after": "{0} after", + "before": "{0} before", + # Ordinals + "ordinal": {"one": "st", "two": "nd", "few": "rd", "other": "th"}, + # Date formats + "date_formats": { + "LTS": "h:mm:ss A", + "LT": "h:mm A", + "L": "MM/DD/YYYY", + "LL": "MMMM D, YYYY", + "LLL": "MMMM D, YYYY h:mm A", + "LLLL": "dddd, MMMM D, YYYY h:mm A", + }, +} diff --git a/src/pendulum/locales/en/locale.py b/src/pendulum/locales/en/locale.py new file mode 100644 index 0000000..4f05c2f --- /dev/null +++ b/src/pendulum/locales/en/locale.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +from pendulum.locales.en.custom import translations as custom_translations + + +""" +en locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "few" + if ( + ((n % 10) == (n % 10) and ((n % 10) == 3)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 13))) + ) + else "one" + if ( + ((n % 10) == (n % 10) and ((n % 10) == 1)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 11))) + ) + else "two" + if ( + ((n % 10) == (n % 10) and ((n % 10) == 2)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 12))) + ) + else "other", + "translations": { + "days": { + "abbreviated": { + 0: "Mon", + 1: "Tue", + 2: "Wed", + 3: "Thu", + 4: "Fri", + 5: "Sat", + 6: "Sun", + }, + "narrow": {0: "M", 1: "T", 2: "W", 3: "T", 4: "F", 5: "S", 6: "S"}, + "short": {0: "Mo", 1: "Tu", 2: "We", 3: "Th", 4: "Fr", 5: "Sa", 6: "Su"}, + "wide": { + 0: "Monday", + 1: "Tuesday", + 2: "Wednesday", + 3: "Thursday", + 4: "Friday", + 5: "Saturday", + 6: "Sunday", + }, + }, + "months": { + "abbreviated": { + 1: "Jan", + 2: "Feb", + 3: "Mar", + 4: "Apr", + 5: "May", + 6: "Jun", + 7: "Jul", + 8: "Aug", + 9: "Sep", + 10: "Oct", + 11: "Nov", + 12: "Dec", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "January", + 2: "February", + 3: "March", + 4: "April", + 5: "May", + 6: "June", + 7: "July", + 8: "August", + 9: "September", + 10: "October", + 11: "November", + 12: "December", + }, + }, + "units": { + "year": {"one": "{0} year", "other": "{0} years"}, + "month": {"one": "{0} month", "other": "{0} months"}, + "week": {"one": "{0} week", "other": "{0} weeks"}, + "day": {"one": "{0} day", "other": "{0} days"}, + "hour": {"one": "{0} hour", "other": "{0} hours"}, + "minute": {"one": "{0} minute", "other": "{0} minutes"}, + "second": {"one": "{0} second", "other": "{0} seconds"}, + "microsecond": {"one": "{0} microsecond", "other": "{0} microseconds"}, + }, + "relative": { + "year": { + "future": {"other": "in {0} years", "one": "in {0} year"}, + "past": {"other": "{0} years ago", "one": "{0} year ago"}, + }, + "month": { + "future": {"other": "in {0} months", "one": "in {0} month"}, + "past": {"other": "{0} months ago", "one": "{0} month ago"}, + }, + "week": { + "future": {"other": "in {0} weeks", "one": "in {0} week"}, + "past": {"other": "{0} weeks ago", "one": "{0} week ago"}, + }, + "day": { + "future": {"other": "in {0} days", "one": "in {0} day"}, + "past": {"other": "{0} days ago", "one": "{0} day ago"}, + }, + "hour": { + "future": {"other": "in {0} hours", "one": "in {0} hour"}, + "past": {"other": "{0} hours ago", "one": "{0} hour ago"}, + }, + "minute": { + "future": {"other": "in {0} minutes", "one": "in {0} minute"}, + "past": {"other": "{0} minutes ago", "one": "{0} minute ago"}, + }, + "second": { + "future": {"other": "in {0} seconds", "one": "in {0} second"}, + "past": {"other": "{0} seconds ago", "one": "{0} second ago"}, + }, + }, + "day_periods": { + "midnight": "midnight", + "am": "AM", + "noon": "noon", + "pm": "PM", + "morning1": "in the morning", + "afternoon1": "in the afternoon", + "evening1": "in the evening", + "night1": "at night", + }, + "week_data": { + "min_days": 1, + "first_day": 6, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/en_gb/__init__.py b/src/pendulum/locales/en_gb/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/en_gb/__init__.py diff --git a/src/pendulum/locales/en_gb/custom.py b/src/pendulum/locales/en_gb/custom.py new file mode 100644 index 0000000..2c77a69 --- /dev/null +++ b/src/pendulum/locales/en_gb/custom.py @@ -0,0 +1,25 @@ +""" +en-gb custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "a few seconds"}, + # Relative time + "ago": "{} ago", + "from_now": "in {}", + "after": "{0} after", + "before": "{0} before", + # Ordinals + "ordinal": {"one": "st", "two": "nd", "few": "rd", "other": "th"}, + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "L": "DD/MM/YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY HH:mm", + "LLLL": "dddd, D MMMM YYYY HH:mm", + }, +} diff --git a/src/pendulum/locales/en_gb/locale.py b/src/pendulum/locales/en_gb/locale.py new file mode 100644 index 0000000..c25f7d5 --- /dev/null +++ b/src/pendulum/locales/en_gb/locale.py @@ -0,0 +1,240 @@ +from __future__ import annotations + +from pendulum.locales.en_gb.custom import translations as custom_translations + + +""" +en-gb locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "few" + if ( + ((n % 10) == (n % 10) and ((n % 10) == 3)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 13))) + ) + else "one" + if ( + ((n % 10) == (n % 10) and ((n % 10) == 1)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 11))) + ) + else "two" + if ( + ((n % 10) == (n % 10) and ((n % 10) == 2)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 12))) + ) + else "other", + "translations": { + "days": { + "abbreviated": { + 0: "Mon", + 1: "Tue", + 2: "Wed", + 3: "Thu", + 4: "Fri", + 5: "Sat", + 6: "Sun", + }, + "narrow": { + 0: "M", + 1: "T", + 2: "W", + 3: "T", + 4: "F", + 5: "S", + 6: "S", + }, + "short": { + 0: "Mo", + 1: "Tu", + 2: "We", + 3: "Th", + 4: "Fr", + 5: "Sa", + 6: "Su", + }, + "wide": { + 0: "Monday", + 1: "Tuesday", + 2: "Wednesday", + 3: "Thursday", + 4: "Friday", + 5: "Saturday", + 6: "Sunday", + }, + }, + "months": { + "abbreviated": { + 1: "Jan", + 2: "Feb", + 3: "Mar", + 4: "Apr", + 5: "May", + 6: "Jun", + 7: "Jul", + 8: "Aug", + 9: "Sept", + 10: "Oct", + 11: "Nov", + 12: "Dec", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "January", + 2: "February", + 3: "March", + 4: "April", + 5: "May", + 6: "June", + 7: "July", + 8: "August", + 9: "September", + 10: "October", + 11: "November", + 12: "December", + }, + }, + "units": { + "year": { + "one": "{0} year", + "other": "{0} years", + }, + "month": { + "one": "{0} month", + "other": "{0} months", + }, + "week": { + "one": "{0} week", + "other": "{0} weeks", + }, + "day": { + "one": "{0} day", + "other": "{0} days", + }, + "hour": { + "one": "{0} hour", + "other": "{0} hours", + }, + "minute": { + "one": "{0} minute", + "other": "{0} minutes", + }, + "second": { + "one": "{0} second", + "other": "{0} seconds", + }, + "microsecond": { + "one": "{0} microsecond", + "other": "{0} microseconds", + }, + }, + "relative": { + "year": { + "future": { + "other": "in {0} years", + "one": "in {0} year", + }, + "past": { + "other": "{0} years ago", + "one": "{0} year ago", + }, + }, + "month": { + "future": { + "other": "in {0} months", + "one": "in {0} month", + }, + "past": { + "other": "{0} months ago", + "one": "{0} month ago", + }, + }, + "week": { + "future": { + "other": "in {0} weeks", + "one": "in {0} week", + }, + "past": { + "other": "{0} weeks ago", + "one": "{0} week ago", + }, + }, + "day": { + "future": { + "other": "in {0} days", + "one": "in {0} day", + }, + "past": { + "other": "{0} days ago", + "one": "{0} day ago", + }, + }, + "hour": { + "future": { + "other": "in {0} hours", + "one": "in {0} hour", + }, + "past": { + "other": "{0} hours ago", + "one": "{0} hour ago", + }, + }, + "minute": { + "future": { + "other": "in {0} minutes", + "one": "in {0} minute", + }, + "past": { + "other": "{0} minutes ago", + "one": "{0} minute ago", + }, + }, + "second": { + "future": { + "other": "in {0} seconds", + "one": "in {0} second", + }, + "past": { + "other": "{0} seconds ago", + "one": "{0} second ago", + }, + }, + }, + "day_periods": { + "midnight": "midnight", + "am": "am", + "noon": "noon", + "pm": "pm", + "morning1": "in the morning", + "afternoon1": "in the afternoon", + "evening1": "in the evening", + "night1": "at night", + }, + "week_data": { + "min_days": 4, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/en_us/__init__.py b/src/pendulum/locales/en_us/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/en_us/__init__.py diff --git a/src/pendulum/locales/en_us/custom.py b/src/pendulum/locales/en_us/custom.py new file mode 100644 index 0000000..72d2005 --- /dev/null +++ b/src/pendulum/locales/en_us/custom.py @@ -0,0 +1,25 @@ +""" +en-us custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "a few seconds"}, + # Relative time + "ago": "{} ago", + "from_now": "in {}", + "after": "{0} after", + "before": "{0} before", + # Ordinals + "ordinal": {"one": "st", "two": "nd", "few": "rd", "other": "th"}, + # Date formats + "date_formats": { + "LTS": "h:mm:ss A", + "LT": "h:mm A", + "L": "MM/DD/YYYY", + "LL": "MMMM D, YYYY", + "LLL": "MMMM D, YYYY h:mm A", + "LLLL": "dddd, MMMM D, YYYY h:mm A", + }, +} diff --git a/src/pendulum/locales/en_us/locale.py b/src/pendulum/locales/en_us/locale.py new file mode 100644 index 0000000..7f40f3f --- /dev/null +++ b/src/pendulum/locales/en_us/locale.py @@ -0,0 +1,240 @@ +from __future__ import annotations + +from pendulum.locales.en_us.custom import translations as custom_translations + + +""" +en-us locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "few" + if ( + ((n % 10) == (n % 10) and ((n % 10) == 3)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 13))) + ) + else "one" + if ( + ((n % 10) == (n % 10) and ((n % 10) == 1)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 11))) + ) + else "two" + if ( + ((n % 10) == (n % 10) and ((n % 10) == 2)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 12))) + ) + else "other", + "translations": { + "days": { + "abbreviated": { + 0: "Mon", + 1: "Tue", + 2: "Wed", + 3: "Thu", + 4: "Fri", + 5: "Sat", + 6: "Sun", + }, + "narrow": { + 0: "M", + 1: "T", + 2: "W", + 3: "T", + 4: "F", + 5: "S", + 6: "S", + }, + "short": { + 0: "Mo", + 1: "Tu", + 2: "We", + 3: "Th", + 4: "Fr", + 5: "Sa", + 6: "Su", + }, + "wide": { + 0: "Monday", + 1: "Tuesday", + 2: "Wednesday", + 3: "Thursday", + 4: "Friday", + 5: "Saturday", + 6: "Sunday", + }, + }, + "months": { + "abbreviated": { + 1: "Jan", + 2: "Feb", + 3: "Mar", + 4: "Apr", + 5: "May", + 6: "Jun", + 7: "Jul", + 8: "Aug", + 9: "Sep", + 10: "Oct", + 11: "Nov", + 12: "Dec", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "January", + 2: "February", + 3: "March", + 4: "April", + 5: "May", + 6: "June", + 7: "July", + 8: "August", + 9: "September", + 10: "October", + 11: "November", + 12: "December", + }, + }, + "units": { + "year": { + "one": "{0} year", + "other": "{0} years", + }, + "month": { + "one": "{0} month", + "other": "{0} months", + }, + "week": { + "one": "{0} week", + "other": "{0} weeks", + }, + "day": { + "one": "{0} day", + "other": "{0} days", + }, + "hour": { + "one": "{0} hour", + "other": "{0} hours", + }, + "minute": { + "one": "{0} minute", + "other": "{0} minutes", + }, + "second": { + "one": "{0} second", + "other": "{0} seconds", + }, + "microsecond": { + "one": "{0} microsecond", + "other": "{0} microseconds", + }, + }, + "relative": { + "year": { + "future": { + "other": "in {0} years", + "one": "in {0} year", + }, + "past": { + "other": "{0} years ago", + "one": "{0} year ago", + }, + }, + "month": { + "future": { + "other": "in {0} months", + "one": "in {0} month", + }, + "past": { + "other": "{0} months ago", + "one": "{0} month ago", + }, + }, + "week": { + "future": { + "other": "in {0} weeks", + "one": "in {0} week", + }, + "past": { + "other": "{0} weeks ago", + "one": "{0} week ago", + }, + }, + "day": { + "future": { + "other": "in {0} days", + "one": "in {0} day", + }, + "past": { + "other": "{0} days ago", + "one": "{0} day ago", + }, + }, + "hour": { + "future": { + "other": "in {0} hours", + "one": "in {0} hour", + }, + "past": { + "other": "{0} hours ago", + "one": "{0} hour ago", + }, + }, + "minute": { + "future": { + "other": "in {0} minutes", + "one": "in {0} minute", + }, + "past": { + "other": "{0} minutes ago", + "one": "{0} minute ago", + }, + }, + "second": { + "future": { + "other": "in {0} seconds", + "one": "in {0} second", + }, + "past": { + "other": "{0} seconds ago", + "one": "{0} second ago", + }, + }, + }, + "day_periods": { + "midnight": "midnight", + "am": "AM", + "noon": "noon", + "pm": "PM", + "morning1": "in the morning", + "afternoon1": "in the afternoon", + "evening1": "in the evening", + "night1": "at night", + }, + "week_data": { + "min_days": 1, + "first_day": 6, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/es/__init__.py b/src/pendulum/locales/es/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/es/__init__.py diff --git a/src/pendulum/locales/es/custom.py b/src/pendulum/locales/es/custom.py new file mode 100644 index 0000000..4acb411 --- /dev/null +++ b/src/pendulum/locales/es/custom.py @@ -0,0 +1,25 @@ +""" +es custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "unos segundos"}, + # Relative time + "ago": "hace {0}", + "from_now": "dentro de {0}", + "after": "{0} después", + "before": "{0} antes", + # Ordinals + "ordinal": {"other": "º"}, + # Date formats + "date_formats": { + "LTS": "H:mm:ss", + "LT": "H:mm", + "LLLL": "dddd, D [de] MMMM [de] YYYY H:mm", + "LLL": "D [de] MMMM [de] YYYY H:mm", + "LL": "D [de] MMMM [de] YYYY", + "L": "DD/MM/YYYY", + }, +} diff --git a/src/pendulum/locales/es/locale.py b/src/pendulum/locales/es/locale.py new file mode 100644 index 0000000..4ab2784 --- /dev/null +++ b/src/pendulum/locales/es/locale.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +from pendulum.locales.es.custom import translations as custom_translations + + +""" +es locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" if (n == n and (n == 1)) else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "lun.", + 1: "mar.", + 2: "mié.", + 3: "jue.", + 4: "vie.", + 5: "sáb.", + 6: "dom.", + }, + "narrow": {0: "L", 1: "M", 2: "X", 3: "J", 4: "V", 5: "S", 6: "D"}, + "short": {0: "LU", 1: "MA", 2: "MI", 3: "JU", 4: "VI", 5: "SA", 6: "DO"}, + "wide": { + 0: "lunes", + 1: "martes", + 2: "miércoles", + 3: "jueves", + 4: "viernes", + 5: "sábado", + 6: "domingo", + }, + }, + "months": { + "abbreviated": { + 1: "ene.", + 2: "feb.", + 3: "mar.", + 4: "abr.", + 5: "may.", + 6: "jun.", + 7: "jul.", + 8: "ago.", + 9: "sept.", + 10: "oct.", + 11: "nov.", + 12: "dic.", + }, + "narrow": { + 1: "E", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "enero", + 2: "febrero", + 3: "marzo", + 4: "abril", + 5: "mayo", + 6: "junio", + 7: "julio", + 8: "agosto", + 9: "septiembre", + 10: "octubre", + 11: "noviembre", + 12: "diciembre", + }, + }, + "units": { + "year": {"one": "{0} año", "other": "{0} años"}, + "month": {"one": "{0} mes", "other": "{0} meses"}, + "week": {"one": "{0} semana", "other": "{0} semanas"}, + "day": {"one": "{0} día", "other": "{0} días"}, + "hour": {"one": "{0} hora", "other": "{0} horas"}, + "minute": {"one": "{0} minuto", "other": "{0} minutos"}, + "second": {"one": "{0} segundo", "other": "{0} segundos"}, + "microsecond": {"one": "{0} microsegundo", "other": "{0} microsegundos"}, + }, + "relative": { + "year": { + "future": {"other": "dentro de {0} años", "one": "dentro de {0} año"}, + "past": {"other": "hace {0} años", "one": "hace {0} año"}, + }, + "month": { + "future": {"other": "dentro de {0} meses", "one": "dentro de {0} mes"}, + "past": {"other": "hace {0} meses", "one": "hace {0} mes"}, + }, + "week": { + "future": { + "other": "dentro de {0} semanas", + "one": "dentro de {0} semana", + }, + "past": {"other": "hace {0} semanas", "one": "hace {0} semana"}, + }, + "day": { + "future": {"other": "dentro de {0} días", "one": "dentro de {0} día"}, + "past": {"other": "hace {0} días", "one": "hace {0} día"}, + }, + "hour": { + "future": {"other": "dentro de {0} horas", "one": "dentro de {0} hora"}, + "past": {"other": "hace {0} horas", "one": "hace {0} hora"}, + }, + "minute": { + "future": { + "other": "dentro de {0} minutos", + "one": "dentro de {0} minuto", + }, + "past": {"other": "hace {0} minutos", "one": "hace {0} minuto"}, + }, + "second": { + "future": { + "other": "dentro de {0} segundos", + "one": "dentro de {0} segundo", + }, + "past": {"other": "hace {0} segundos", "one": "hace {0} segundo"}, + }, + }, + "day_periods": { + "am": "a. m.", + "noon": "del mediodía", + "pm": "p. m.", + "morning1": "de la madrugada", + "morning2": "de la mañana", + "evening1": "de la tarde", + "night1": "de la noche", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/fa/__init__.py b/src/pendulum/locales/fa/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/fa/__init__.py diff --git a/src/pendulum/locales/fa/custom.py b/src/pendulum/locales/fa/custom.py new file mode 100644 index 0000000..e4b4a60 --- /dev/null +++ b/src/pendulum/locales/fa/custom.py @@ -0,0 +1,20 @@ +""" +fa custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "after": "{0} پس از", + "before": "{0} پیش از", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd, D MMMM YYYY HH:mm", + "LLL": "D MMMM YYYY HH:mm", + "LL": "D MMMM YYYY", + "L": "DD/MM/YYYY", + }, +} diff --git a/src/pendulum/locales/fa/locale.py b/src/pendulum/locales/fa/locale.py new file mode 100644 index 0000000..1c3f6c5 --- /dev/null +++ b/src/pendulum/locales/fa/locale.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +from pendulum.locales.fa.custom import translations as custom_translations + + +""" +fa locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and (n == 0)) or (n == n and (n == 1))) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "دوشنبه", + 1: "سه\u200cشنبه", + 2: "چهارشنبه", + 3: "پنجشنبه", + 4: "جمعه", + 5: "شنبه", + 6: "یکشنبه", + }, + "narrow": {0: "د", 1: "س", 2: "چ", 3: "پ", 4: "ج", 5: "ش", 6: "ی"}, + "short": {0: "۲ش", 1: "۳ش", 2: "۴ش", 3: "۵ش", 4: "ج", 5: "ش", 6: "۱ش"}, + "wide": { + 0: "دوشنبه", + 1: "سه\u200cشنبه", + 2: "چهارشنبه", + 3: "پنجشنبه", + 4: "جمعه", + 5: "شنبه", + 6: "یکشنبه", + }, + }, + "months": { + "abbreviated": { + 1: "ژانویهٔ", + 2: "فوریهٔ", + 3: "مارس", + 4: "آوریل", + 5: "مهٔ", + 6: "ژوئن", + 7: "ژوئیهٔ", + 8: "اوت", + 9: "سپتامبر", + 10: "اکتبر", + 11: "نوامبر", + 12: "دسامبر", + }, + "narrow": { + 1: "ژ", + 2: "ف", + 3: "م", + 4: "آ", + 5: "م", + 6: "ژ", + 7: "ژ", + 8: "ا", + 9: "س", + 10: "ا", + 11: "ن", + 12: "د", + }, + "wide": { + 1: "ژانویهٔ", + 2: "فوریهٔ", + 3: "مارس", + 4: "آوریل", + 5: "مهٔ", + 6: "ژوئن", + 7: "ژوئیهٔ", + 8: "اوت", + 9: "سپتامبر", + 10: "اکتبر", + 11: "نوامبر", + 12: "دسامبر", + }, + }, + "units": { + "year": {"one": "{0} سال", "other": "{0} سال"}, + "month": {"one": "{0} ماه", "other": "{0} ماه"}, + "week": {"one": "{0} هفته", "other": "{0} هفته"}, + "day": {"one": "{0} روز", "other": "{0} روز"}, + "hour": {"one": "{0} ساعت", "other": "{0} ساعت"}, + "minute": {"one": "{0} دقیقه", "other": "{0} دقیقه"}, + "second": {"one": "{0} ثانیه", "other": "{0} ثانیه"}, + "microsecond": {"one": "{0} میکروثانیه", "other": "{0} میکروثانیه"}, + }, + "relative": { + "year": { + "future": {"other": "{0} سال بعد", "one": "{0} سال بعد"}, + "past": {"other": "{0} سال پیش", "one": "{0} سال پیش"}, + }, + "month": { + "future": {"other": "{0} ماه بعد", "one": "{0} ماه بعد"}, + "past": {"other": "{0} ماه پیش", "one": "{0} ماه پیش"}, + }, + "week": { + "future": {"other": "{0} هفته بعد", "one": "{0} هفته بعد"}, + "past": {"other": "{0} هفته پیش", "one": "{0} هفته پیش"}, + }, + "day": { + "future": {"other": "{0} روز بعد", "one": "{0} روز بعد"}, + "past": {"other": "{0} روز پیش", "one": "{0} روز پیش"}, + }, + "hour": { + "future": {"other": "{0} ساعت بعد", "one": "{0} ساعت بعد"}, + "past": {"other": "{0} ساعت پیش", "one": "{0} ساعت پیش"}, + }, + "minute": { + "future": {"other": "{0} دقیقه بعد", "one": "{0} دقیقه بعد"}, + "past": {"other": "{0} دقیقه پیش", "one": "{0} دقیقه پیش"}, + }, + "second": { + "future": {"other": "{0} ثانیه بعد", "one": "{0} ثانیه بعد"}, + "past": {"other": "{0} ثانیه پیش", "one": "{0} ثانیه پیش"}, + }, + }, + "day_periods": { + "midnight": "نیمه\u200cشب", + "am": "قبل\u200cازظهر", + "noon": "ظهر", + "pm": "بعدازظهر", + "morning1": "صبح", + "afternoon1": "عصر", + "evening1": "عصر", + "night1": "شب", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/fo/__init__.py b/src/pendulum/locales/fo/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/fo/__init__.py diff --git a/src/pendulum/locales/fo/custom.py b/src/pendulum/locales/fo/custom.py new file mode 100644 index 0000000..3f0fd1c --- /dev/null +++ b/src/pendulum/locales/fo/custom.py @@ -0,0 +1,22 @@ +""" +fo custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "after": "{0} aftaná", + "before": "{0} áðrenn", + # Ordinals + "ordinal": {"other": "."}, + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd D. MMMM, YYYY HH:mm", + "LLL": "D MMMM YYYY HH:mm", + "LL": "D MMMM YYYY", + "L": "DD/MM/YYYY", + }, +} diff --git a/src/pendulum/locales/fo/locale.py b/src/pendulum/locales/fo/locale.py new file mode 100644 index 0000000..28ec0c0 --- /dev/null +++ b/src/pendulum/locales/fo/locale.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +from pendulum.locales.fo.custom import translations as custom_translations + + +""" +fo locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" if (n == n and (n == 1)) else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "mán.", + 1: "týs.", + 2: "mik.", + 3: "hós.", + 4: "frí.", + 5: "ley.", + 6: "sun.", + }, + "narrow": {0: "M", 1: "T", 2: "M", 3: "H", 4: "F", 5: "L", 6: "S"}, + "short": { + 0: "má.", + 1: "tý.", + 2: "mi.", + 3: "hó.", + 4: "fr.", + 5: "le.", + 6: "su.", + }, + "wide": { + 0: "mánadagur", + 1: "týsdagur", + 2: "mikudagur", + 3: "hósdagur", + 4: "fríggjadagur", + 5: "leygardagur", + 6: "sunnudagur", + }, + }, + "months": { + "abbreviated": { + 1: "jan.", + 2: "feb.", + 3: "mar.", + 4: "apr.", + 5: "mai", + 6: "jun.", + 7: "jul.", + 8: "aug.", + 9: "sep.", + 10: "okt.", + 11: "nov.", + 12: "des.", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "januar", + 2: "februar", + 3: "mars", + 4: "apríl", + 5: "mai", + 6: "juni", + 7: "juli", + 8: "august", + 9: "september", + 10: "oktober", + 11: "november", + 12: "desember", + }, + }, + "units": { + "year": {"one": "{0} ár", "other": "{0} ár"}, + "month": {"one": "{0} mánaður", "other": "{0} mánaðir"}, + "week": {"one": "{0} vika", "other": "{0} vikur"}, + "day": {"one": "{0} dagur", "other": "{0} dagar"}, + "hour": {"one": "{0} tími", "other": "{0} tímar"}, + "minute": {"one": "{0} minuttur", "other": "{0} minuttir"}, + "second": {"one": "{0} sekund", "other": "{0} sekundir"}, + "microsecond": {"one": "{0} mikrosekund", "other": "{0} mikrosekundir"}, + }, + "relative": { + "year": { + "future": {"other": "um {0} ár", "one": "um {0} ár"}, + "past": {"other": "{0} ár síðan", "one": "{0} ár síðan"}, + }, + "month": { + "future": {"other": "um {0} mánaðir", "one": "um {0} mánað"}, + "past": {"other": "{0} mánaðir síðan", "one": "{0} mánað síðan"}, + }, + "week": { + "future": {"other": "um {0} vikur", "one": "um {0} viku"}, + "past": {"other": "{0} vikur síðan", "one": "{0} vika síðan"}, + }, + "day": { + "future": {"other": "um {0} dagar", "one": "um {0} dag"}, + "past": {"other": "{0} dagar síðan", "one": "{0} dagur síðan"}, + }, + "hour": { + "future": {"other": "um {0} tímar", "one": "um {0} tíma"}, + "past": {"other": "{0} tímar síðan", "one": "{0} tími síðan"}, + }, + "minute": { + "future": {"other": "um {0} minuttir", "one": "um {0} minutt"}, + "past": {"other": "{0} minuttir síðan", "one": "{0} minutt síðan"}, + }, + "second": { + "future": {"other": "um {0} sekund", "one": "um {0} sekund"}, + "past": {"other": "{0} sekund síðan", "one": "{0} sekund síðan"}, + }, + }, + "day_periods": {"am": "AM", "pm": "PM"}, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/fr/__init__.py b/src/pendulum/locales/fr/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/fr/__init__.py diff --git a/src/pendulum/locales/fr/custom.py b/src/pendulum/locales/fr/custom.py new file mode 100644 index 0000000..913656c --- /dev/null +++ b/src/pendulum/locales/fr/custom.py @@ -0,0 +1,25 @@ +""" +fr custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "quelques secondes"}, + # Relative Time + "ago": "il y a {0}", + "from_now": "dans {0}", + "after": "{0} après", + "before": "{0} avant", + # Ordinals + "ordinal": {"one": "er", "other": "e"}, + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd D MMMM YYYY HH:mm", + "LLL": "D MMMM YYYY HH:mm", + "LL": "D MMMM YYYY", + "L": "DD/MM/YYYY", + }, +} diff --git a/src/pendulum/locales/fr/locale.py b/src/pendulum/locales/fr/locale.py new file mode 100644 index 0000000..30ee8cd --- /dev/null +++ b/src/pendulum/locales/fr/locale.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +from pendulum.locales.fr.custom import translations as custom_translations + + +""" +fr locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" if (n == n and ((n == 0) or (n == 1))) else "other", + "ordinal": lambda n: "one" if (n == n and (n == 1)) else "other", + "translations": { + "days": { + "abbreviated": { + 0: "lun.", + 1: "mar.", + 2: "mer.", + 3: "jeu.", + 4: "ven.", + 5: "sam.", + 6: "dim.", + }, + "narrow": {0: "L", 1: "M", 2: "M", 3: "J", 4: "V", 5: "S", 6: "D"}, + "short": {0: "lu", 1: "ma", 2: "me", 3: "je", 4: "ve", 5: "sa", 6: "di"}, + "wide": { + 0: "lundi", + 1: "mardi", + 2: "mercredi", + 3: "jeudi", + 4: "vendredi", + 5: "samedi", + 6: "dimanche", + }, + }, + "months": { + "abbreviated": { + 1: "janv.", + 2: "févr.", + 3: "mars", + 4: "avr.", + 5: "mai", + 6: "juin", + 7: "juil.", + 8: "août", + 9: "sept.", + 10: "oct.", + 11: "nov.", + 12: "déc.", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "janvier", + 2: "février", + 3: "mars", + 4: "avril", + 5: "mai", + 6: "juin", + 7: "juillet", + 8: "août", + 9: "septembre", + 10: "octobre", + 11: "novembre", + 12: "décembre", + }, + }, + "units": { + "year": {"one": "{0} an", "other": "{0} ans"}, + "month": {"one": "{0} mois", "other": "{0} mois"}, + "week": {"one": "{0} semaine", "other": "{0} semaines"}, + "day": {"one": "{0} jour", "other": "{0} jours"}, + "hour": {"one": "{0} heure", "other": "{0} heures"}, + "minute": {"one": "{0} minute", "other": "{0} minutes"}, + "second": {"one": "{0} seconde", "other": "{0} secondes"}, + "microsecond": {"one": "{0} microseconde", "other": "{0} microsecondes"}, + }, + "relative": { + "year": { + "future": {"other": "dans {0} ans", "one": "dans {0} an"}, + "past": {"other": "il y a {0} ans", "one": "il y a {0} an"}, + }, + "month": { + "future": {"other": "dans {0} mois", "one": "dans {0} mois"}, + "past": {"other": "il y a {0} mois", "one": "il y a {0} mois"}, + }, + "week": { + "future": {"other": "dans {0} semaines", "one": "dans {0} semaine"}, + "past": {"other": "il y a {0} semaines", "one": "il y a {0} semaine"}, + }, + "day": { + "future": {"other": "dans {0} jours", "one": "dans {0} jour"}, + "past": {"other": "il y a {0} jours", "one": "il y a {0} jour"}, + }, + "hour": { + "future": {"other": "dans {0} heures", "one": "dans {0} heure"}, + "past": {"other": "il y a {0} heures", "one": "il y a {0} heure"}, + }, + "minute": { + "future": {"other": "dans {0} minutes", "one": "dans {0} minute"}, + "past": {"other": "il y a {0} minutes", "one": "il y a {0} minute"}, + }, + "second": { + "future": {"other": "dans {0} secondes", "one": "dans {0} seconde"}, + "past": {"other": "il y a {0} secondes", "one": "il y a {0} seconde"}, + }, + }, + "day_periods": { + "midnight": "minuit", + "am": "AM", + "noon": "midi", + "pm": "PM", + "morning1": "du matin", + "afternoon1": "de l’après-midi", + "evening1": "du soir", + "night1": "de nuit", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/he/__init__.py b/src/pendulum/locales/he/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/he/__init__.py diff --git a/src/pendulum/locales/he/custom.py b/src/pendulum/locales/he/custom.py new file mode 100644 index 0000000..c8e1f70 --- /dev/null +++ b/src/pendulum/locales/he/custom.py @@ -0,0 +1,25 @@ +""" +he custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "כמה שניות"}, + # Relative time + "ago": "לפני {0}", + "from_now": "תוך {0}", + "after": "בעוד {0}", + "before": "{0} קודם", + # Ordinals + "ordinal": {"other": "º"}, + # Date formats + "date_formats": { + "LTS": "H:mm:ss", + "LT": "H:mm", + "LLLL": "dddd, D [ב] MMMM [ב] YYYY H:mm", + "LLL": "D [ב] MMMM [ב] YYYY H:mm", + "LL": "D [ב] MMMM [ב] YYYY", + "L": "DD/MM/YYYY", + }, +} diff --git a/src/pendulum/locales/he/locale.py b/src/pendulum/locales/he/locale.py new file mode 100644 index 0000000..42d55aa --- /dev/null +++ b/src/pendulum/locales/he/locale.py @@ -0,0 +1,277 @@ +from __future__ import annotations + +from pendulum.locales.he.custom import translations as custom_translations + + +""" +he locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "many" + if ( + ((0 == 0 and (0 == 0)) and (not (n == n and (n >= 0 and n <= 10)))) + and ((n % 10) == (n % 10) and ((n % 10) == 0)) + ) + else "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "two" + if ((n == n and (n == 2)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "יום ב׳", + 1: "יום ג׳", + 2: "יום ד׳", + 3: "יום ה׳", + 4: "יום ו׳", + 5: "שבת", + 6: "יום א׳", + }, + "narrow": { + 0: "ב׳", + 1: "ג׳", + 2: "ד׳", + 3: "ה׳", + 4: "ו׳", + 5: "ש׳", + 6: "א׳", + }, + "short": { + 0: "ב׳", + 1: "ג׳", + 2: "ד׳", + 3: "ה׳", + 4: "ו׳", + 5: "ש׳", + 6: "א׳", + }, + "wide": { + 0: "יום שני", + 1: "יום שלישי", + 2: "יום רביעי", + 3: "יום חמישי", + 4: "יום שישי", + 5: "יום שבת", + 6: "יום ראשון", + }, + }, + "months": { + "abbreviated": { + 1: "ינו׳", + 2: "פבר׳", + 3: "מרץ", + 4: "אפר׳", + 5: "מאי", + 6: "יוני", + 7: "יולי", + 8: "אוג׳", + 9: "ספט׳", + 10: "אוק׳", + 11: "נוב׳", + 12: "דצמ׳", + }, + "narrow": { + 1: "1", + 2: "2", + 3: "3", + 4: "4", + 5: "5", + 6: "6", + 7: "7", + 8: "8", + 9: "9", + 10: "10", + 11: "11", + 12: "12", + }, + "wide": { + 1: "ינואר", + 2: "פברואר", + 3: "מרץ", + 4: "אפריל", + 5: "מאי", + 6: "יוני", + 7: "יולי", + 8: "אוגוסט", + 9: "ספטמבר", + 10: "אוקטובר", + 11: "נובמבר", + 12: "דצמבר", + }, + }, + "units": { + "year": { + "one": "שנה", + "two": "שנתיים", + "many": "{0} שנים", + "other": "{0} שנים", + }, + "month": { + "one": "חודש", + "two": "חודשיים", + "many": "{0} חודשים", + "other": "{0} חודשים", + }, + "week": { + "one": "שבוע", + "two": "שבועיים", + "many": "{0} שבועות", + "other": "{0} שבועות", + }, + "day": { + "one": "יום {0}", + "two": "יומיים", + "many": "{0} יום", + "other": "{0} ימים", + }, + "hour": { + "one": "שעה", + "two": "שעתיים", + "many": "{0} שעות", + "other": "{0} שעות", + }, + "minute": { + "one": "דקה", + "two": "שתי דקות", + "many": "{0} דקות", + "other": "{0} דקות", + }, + "second": { + "one": "שניה", + "two": "שתי שניות", + "many": "\u200f{0} שניות", + "other": "{0} שניות", + }, + "microsecond": { + "one": "{0} מיליונית שנייה", + "two": "{0} מיליוניות שנייה", + "many": "{0} מיליוניות שנייה", + "other": "{0} מיליוניות שנייה", + }, + }, + "relative": { + "year": { + "future": { + "other": "בעוד {0} שנים", + "one": "בעוד שנה", + "two": "בעוד שנתיים", + "many": "בעוד {0} שנה", + }, + "past": { + "other": "לפני {0} שנים", + "one": "לפני שנה", + "two": "לפני שנתיים", + "many": "לפני {0} שנה", + }, + }, + "month": { + "future": { + "other": "בעוד {0} חודשים", + "one": "בעוד חודש", + "two": "בעוד חודשיים", + "many": "בעוד {0} חודשים", + }, + "past": { + "other": "לפני {0} חודשים", + "one": "לפני חודש", + "two": "לפני חודשיים", + "many": "לפני {0} חודשים", + }, + }, + "week": { + "future": { + "other": "בעוד {0} שבועות", + "one": "בעוד שבוע", + "two": "בעוד שבועיים", + "many": "בעוד {0} שבועות", + }, + "past": { + "other": "לפני {0} שבועות", + "one": "לפני שבוע", + "two": "לפני שבועיים", + "many": "לפני {0} שבועות", + }, + }, + "day": { + "future": { + "other": "בעוד {0} ימים", + "one": "בעוד יום {0}", + "two": "בעוד יומיים", + "many": "בעוד {0} ימים", + }, + "past": { + "other": "לפני {0} ימים", + "one": "לפני יום {0}", + "two": "לפני יומיים", + "many": "לפני {0} ימים", + }, + }, + "hour": { + "future": { + "other": "בעוד {0} שעות", + "one": "בעוד שעה", + "two": "בעוד שעתיים", + "many": "בעוד {0} שעות", + }, + "past": { + "other": "לפני {0} שעות", + "one": "לפני שעה", + "two": "לפני שעתיים", + "many": "לפני {0} שעות", + }, + }, + "minute": { + "future": { + "other": "בעוד {0} דקות", + "one": "בעוד דקה", + "two": "בעוד שתי דקות", + "many": "בעוד {0} דקות", + }, + "past": { + "other": "לפני {0} דקות", + "one": "לפני דקה", + "two": "לפני שתי דקות", + "many": "לפני {0} דקות", + }, + }, + "second": { + "future": { + "other": "בעוד {0} שניות", + "one": "בעוד שנייה", + "two": "בעוד שתי שניות", + "many": "בעוד {0} שניות", + }, + "past": { + "other": "לפני {0} שניות", + "one": "לפני שנייה", + "two": "לפני שתי שניות", + "many": "לפני {0} שניות", + }, + }, + }, + "day_periods": { + "midnight": "חצות", + "am": "לפנה״צ", + "pm": "אחה״צ", + "morning1": "בבוקר", + "afternoon1": "בצהריים", + "afternoon2": "אחר הצהריים", + "evening1": "בערב", + "night1": "בלילה", + "night2": "לפנות בוקר", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/id/__init__.py b/src/pendulum/locales/id/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/id/__init__.py diff --git a/src/pendulum/locales/id/custom.py b/src/pendulum/locales/id/custom.py new file mode 100644 index 0000000..3d4460c --- /dev/null +++ b/src/pendulum/locales/id/custom.py @@ -0,0 +1,21 @@ +""" +id custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "beberapa detik"}, + "ago": "{} yang lalu", + "from_now": "dalam {}", + "after": "{0} kemudian", + "before": "{0} yang lalu", + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd [d.] D. MMMM YYYY HH:mm", + "LLL": "D. MMMM YYYY HH:mm", + "LL": "D. MMMM YYYY", + "L": "DD/MM/YYYY", + }, +} diff --git a/src/pendulum/locales/id/locale.py b/src/pendulum/locales/id/locale.py new file mode 100644 index 0000000..2073357 --- /dev/null +++ b/src/pendulum/locales/id/locale.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +from pendulum.locales.id.custom import translations as custom_translations + + +""" +id locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "Sen", + 1: "Sel", + 2: "Rab", + 3: "Kam", + 4: "Jum", + 5: "Sab", + 6: "Min", + }, + "narrow": {0: "S", 1: "S", 2: "R", 3: "K", 4: "J", 5: "S", 6: "M"}, + "short": { + 0: "Sen", + 1: "Sel", + 2: "Rab", + 3: "Kam", + 4: "Jum", + 5: "Sab", + 6: "Min", + }, + "wide": { + 0: "Senin", + 1: "Selasa", + 2: "Rabu", + 3: "Kamis", + 4: "Jumat", + 5: "Sabtu", + 6: "Minggu", + }, + }, + "months": { + "abbreviated": { + 1: "Jan", + 2: "Feb", + 3: "Mar", + 4: "Apr", + 5: "Mei", + 6: "Jun", + 7: "Jul", + 8: "Agt", + 9: "Sep", + 10: "Okt", + 11: "Nov", + 12: "Des", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "Januari", + 2: "Februari", + 3: "Maret", + 4: "April", + 5: "Mei", + 6: "Juni", + 7: "Juli", + 8: "Agustus", + 9: "September", + 10: "Oktober", + 11: "November", + 12: "Desember", + }, + }, + "units": { + "year": {"other": "{0} tahun"}, + "month": {"other": "{0} bulan"}, + "week": {"other": "{0} minggu"}, + "day": {"other": "{0} hari"}, + "hour": {"other": "{0} jam"}, + "minute": {"other": "{0} menit"}, + "second": {"other": "{0} detik"}, + "microsecond": {"other": "{0} mikrodetik"}, + }, + "relative": { + "year": { + "future": {"other": "dalam {0} tahun"}, + "past": {"other": "{0} tahun yang lalu"}, + }, + "month": { + "future": {"other": "dalam {0} bulan"}, + "past": {"other": "{0} bulan yang lalu"}, + }, + "week": { + "future": {"other": "dalam {0} minggu"}, + "past": {"other": "{0} minggu yang lalu"}, + }, + "day": { + "future": {"other": "dalam {0} hari"}, + "past": {"other": "{0} hari yang lalu"}, + }, + "hour": { + "future": {"other": "dalam {0} jam"}, + "past": {"other": "{0} jam yang lalu"}, + }, + "minute": { + "future": {"other": "dalam {0} menit"}, + "past": {"other": "{0} menit yang lalu"}, + }, + "second": { + "future": {"other": "dalam {0} detik"}, + "past": {"other": "{0} detik yang lalu"}, + }, + }, + "day_periods": { + "midnight": "tengah malam", + "am": "AM", + "noon": "tengah hari", + "pm": "PM", + "morning1": "pagi", + "afternoon1": "siang", + "evening1": "sore", + "night1": "malam", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/it/__init__.py b/src/pendulum/locales/it/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/it/__init__.py diff --git a/src/pendulum/locales/it/custom.py b/src/pendulum/locales/it/custom.py new file mode 100644 index 0000000..b1a77a0 --- /dev/null +++ b/src/pendulum/locales/it/custom.py @@ -0,0 +1,25 @@ +""" +it custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "alcuni secondi"}, + # Relative Time + "ago": "{0} fa", + "from_now": "in {0}", + "after": "{0} dopo", + "before": "{0} prima", + # Ordinals + "ordinal": {"other": "°"}, + # Date formats + "date_formats": { + "LTS": "H:mm:ss", + "LT": "H:mm", + "L": "DD/MM/YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY [alle] H:mm", + "LLLL": "dddd, D MMMM YYYY [alle] H:mm", + }, +} diff --git a/src/pendulum/locales/it/locale.py b/src/pendulum/locales/it/locale.py new file mode 100644 index 0000000..ae5dc39 --- /dev/null +++ b/src/pendulum/locales/it/locale.py @@ -0,0 +1,153 @@ +from __future__ import annotations + +from pendulum.locales.it.custom import translations as custom_translations + + +""" +it locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "many" + if (n == n and ((n == 11) or (n == 8) or (n == 80) or (n == 800))) + else "other", + "translations": { + "days": { + "abbreviated": { + 0: "lun", + 1: "mar", + 2: "mer", + 3: "gio", + 4: "ven", + 5: "sab", + 6: "dom", + }, + "narrow": {0: "L", 1: "M", 2: "M", 3: "G", 4: "V", 5: "S", 6: "D"}, + "short": { + 0: "lun", + 1: "mar", + 2: "mer", + 3: "gio", + 4: "ven", + 5: "sab", + 6: "dom", + }, + "wide": { + 0: "lunedì", + 1: "martedì", + 2: "mercoledì", + 3: "giovedì", + 4: "venerdì", + 5: "sabato", + 6: "domenica", + }, + }, + "months": { + "abbreviated": { + 1: "gen", + 2: "feb", + 3: "mar", + 4: "apr", + 5: "mag", + 6: "giu", + 7: "lug", + 8: "ago", + 9: "set", + 10: "ott", + 11: "nov", + 12: "dic", + }, + "narrow": { + 1: "G", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "G", + 7: "L", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "gennaio", + 2: "febbraio", + 3: "marzo", + 4: "aprile", + 5: "maggio", + 6: "giugno", + 7: "luglio", + 8: "agosto", + 9: "settembre", + 10: "ottobre", + 11: "novembre", + 12: "dicembre", + }, + }, + "units": { + "year": {"one": "{0} anno", "other": "{0} anni"}, + "month": {"one": "{0} mese", "other": "{0} mesi"}, + "week": {"one": "{0} settimana", "other": "{0} settimane"}, + "day": {"one": "{0} giorno", "other": "{0} giorni"}, + "hour": {"one": "{0} ora", "other": "{0} ore"}, + "minute": {"one": "{0} minuto", "other": "{0} minuti"}, + "second": {"one": "{0} secondo", "other": "{0} secondi"}, + "microsecond": {"one": "{0} microsecondo", "other": "{0} microsecondi"}, + }, + "relative": { + "year": { + "future": {"other": "tra {0} anni", "one": "tra {0} anno"}, + "past": {"other": "{0} anni fa", "one": "{0} anno fa"}, + }, + "month": { + "future": {"other": "tra {0} mesi", "one": "tra {0} mese"}, + "past": {"other": "{0} mesi fa", "one": "{0} mese fa"}, + }, + "week": { + "future": {"other": "tra {0} settimane", "one": "tra {0} settimana"}, + "past": {"other": "{0} settimane fa", "one": "{0} settimana fa"}, + }, + "day": { + "future": {"other": "tra {0} giorni", "one": "tra {0} giorno"}, + "past": {"other": "{0} giorni fa", "one": "{0} giorno fa"}, + }, + "hour": { + "future": {"other": "tra {0} ore", "one": "tra {0} ora"}, + "past": {"other": "{0} ore fa", "one": "{0} ora fa"}, + }, + "minute": { + "future": {"other": "tra {0} minuti", "one": "tra {0} minuto"}, + "past": {"other": "{0} minuti fa", "one": "{0} minuto fa"}, + }, + "second": { + "future": {"other": "tra {0} secondi", "one": "tra {0} secondo"}, + "past": {"other": "{0} secondi fa", "one": "{0} secondo fa"}, + }, + }, + "day_periods": { + "midnight": "mezzanotte", + "am": "AM", + "noon": "mezzogiorno", + "pm": "PM", + "morning1": "di mattina", + "afternoon1": "del pomeriggio", + "evening1": "di sera", + "night1": "di notte", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/ja/__init__.py b/src/pendulum/locales/ja/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/ja/__init__.py diff --git a/src/pendulum/locales/ja/custom.py b/src/pendulum/locales/ja/custom.py new file mode 100644 index 0000000..4cb5b95 --- /dev/null +++ b/src/pendulum/locales/ja/custom.py @@ -0,0 +1,23 @@ +""" +ja custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "数秒"}, + # Relative time + "ago": "{} 前に", + "from_now": "今から {}", + "after": "{0} 後", + "before": "{0} 前", + # Date formats + "date_formats": { + "LTS": "h:mm:ss A", + "LT": "h:mm A", + "L": "MM/DD/YYYY", + "LL": "MMMM D, YYYY", + "LLL": "MMMM D, YYYY h:mm A", + "LLLL": "dddd, MMMM D, YYYY h:mm A", + }, +} diff --git a/src/pendulum/locales/ja/locale.py b/src/pendulum/locales/ja/locale.py new file mode 100644 index 0000000..a1d3bd9 --- /dev/null +++ b/src/pendulum/locales/ja/locale.py @@ -0,0 +1,202 @@ +from __future__ import annotations + +from pendulum.locales.ja.custom import translations as custom_translations + + +""" +ja locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "月", + 1: "火", + 2: "水", + 3: "木", + 4: "金", + 5: "土", + 6: "日", + }, + "narrow": { + 0: "月", + 1: "火", + 2: "水", + 3: "木", + 4: "金", + 5: "土", + 6: "日", + }, + "short": { + 0: "月", + 1: "火", + 2: "水", + 3: "木", + 4: "金", + 5: "土", + 6: "日", + }, + "wide": { + 0: "月曜日", + 1: "火曜日", + 2: "水曜日", + 3: "木曜日", + 4: "金曜日", + 5: "土曜日", + 6: "日曜日", + }, + }, + "months": { + "abbreviated": { + 1: "1月", + 2: "2月", + 3: "3月", + 4: "4月", + 5: "5月", + 6: "6月", + 7: "7月", + 8: "8月", + 9: "9月", + 10: "10月", + 11: "11月", + 12: "12月", + }, + "narrow": { + 1: "1", + 2: "2", + 3: "3", + 4: "4", + 5: "5", + 6: "6", + 7: "7", + 8: "8", + 9: "9", + 10: "10", + 11: "11", + 12: "12", + }, + "wide": { + 1: "1月", + 2: "2月", + 3: "3月", + 4: "4月", + 5: "5月", + 6: "6月", + 7: "7月", + 8: "8月", + 9: "9月", + 10: "10月", + 11: "11月", + 12: "12月", + }, + }, + "units": { + "year": { + "other": "{0} 年", + }, + "month": { + "other": "{0} か月", + }, + "week": { + "other": "{0} 週間", + }, + "day": { + "other": "{0} 日", + }, + "hour": { + "other": "{0} 時間", + }, + "minute": { + "other": "{0} 分", + }, + "second": { + "other": "{0} 秒", + }, + "microsecond": { + "other": "{0} マイクロ秒", + }, + }, + "relative": { + "year": { + "future": { + "other": "{0} 年後", + }, + "past": { + "other": "{0} 年前", + }, + }, + "month": { + "future": { + "other": "{0} か月後", + }, + "past": { + "other": "{0} か月前", + }, + }, + "week": { + "future": { + "other": "{0} 週間後", + }, + "past": { + "other": "{0} 週間前", + }, + }, + "day": { + "future": { + "other": "{0} 日後", + }, + "past": { + "other": "{0} 日前", + }, + }, + "hour": { + "future": { + "other": "{0} 時間後", + }, + "past": { + "other": "{0} 時間前", + }, + }, + "minute": { + "future": { + "other": "{0} 分後", + }, + "past": { + "other": "{0} 分前", + }, + }, + "second": { + "future": { + "other": "{0} 秒後", + }, + "past": { + "other": "{0} 秒前", + }, + }, + }, + "day_periods": { + "midnight": "真夜中", + "am": "午前", + "noon": "正午", + "pm": "午後", + "morning1": "朝", + "afternoon1": "昼", + "evening1": "夕方", + "night1": "夜", + "night2": "夜中", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/ko/__init__.py b/src/pendulum/locales/ko/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/ko/__init__.py diff --git a/src/pendulum/locales/ko/custom.py b/src/pendulum/locales/ko/custom.py new file mode 100644 index 0000000..b7476ff --- /dev/null +++ b/src/pendulum/locales/ko/custom.py @@ -0,0 +1,20 @@ +""" +ko custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "after": "{0} 뒤", + "before": "{0} 앞", + # Date formats + "date_formats": { + "LTS": "A h시 m분 s초", + "LT": "A h시 m분", + "LLLL": "YYYY년 MMMM D일 dddd A h시 m분", + "LLL": "YYYY년 MMMM D일 A h시 m분", + "LL": "YYYY년 MMMM D일", + "L": "YYYY.MM.DD", + }, +} diff --git a/src/pendulum/locales/ko/locale.py b/src/pendulum/locales/ko/locale.py new file mode 100644 index 0000000..b285dc0 --- /dev/null +++ b/src/pendulum/locales/ko/locale.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +from pendulum.locales.ko.custom import translations as custom_translations + + +""" +ko locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": {0: "월", 1: "화", 2: "수", 3: "목", 4: "금", 5: "토", 6: "일"}, + "narrow": {0: "월", 1: "화", 2: "수", 3: "목", 4: "금", 5: "토", 6: "일"}, + "short": {0: "월", 1: "화", 2: "수", 3: "목", 4: "금", 5: "토", 6: "일"}, + "wide": { + 0: "월요일", + 1: "화요일", + 2: "수요일", + 3: "목요일", + 4: "금요일", + 5: "토요일", + 6: "일요일", + }, + }, + "months": { + "abbreviated": { + 1: "1월", + 2: "2월", + 3: "3월", + 4: "4월", + 5: "5월", + 6: "6월", + 7: "7월", + 8: "8월", + 9: "9월", + 10: "10월", + 11: "11월", + 12: "12월", + }, + "narrow": { + 1: "1월", + 2: "2월", + 3: "3월", + 4: "4월", + 5: "5월", + 6: "6월", + 7: "7월", + 8: "8월", + 9: "9월", + 10: "10월", + 11: "11월", + 12: "12월", + }, + "wide": { + 1: "1월", + 2: "2월", + 3: "3월", + 4: "4월", + 5: "5월", + 6: "6월", + 7: "7월", + 8: "8월", + 9: "9월", + 10: "10월", + 11: "11월", + 12: "12월", + }, + }, + "units": { + "year": {"other": "{0}년"}, + "month": {"other": "{0}개월"}, + "week": {"other": "{0}주"}, + "day": {"other": "{0}일"}, + "hour": {"other": "{0}시간"}, + "minute": {"other": "{0}분"}, + "second": {"other": "{0}초"}, + "microsecond": {"other": "{0}마이크로초"}, + }, + "relative": { + "year": {"future": {"other": "{0}년 후"}, "past": {"other": "{0}년 전"}}, + "month": {"future": {"other": "{0}개월 후"}, "past": {"other": "{0}개월 전"}}, + "week": {"future": {"other": "{0}주 후"}, "past": {"other": "{0}주 전"}}, + "day": {"future": {"other": "{0}일 후"}, "past": {"other": "{0}일 전"}}, + "hour": {"future": {"other": "{0}시간 후"}, "past": {"other": "{0}시간 전"}}, + "minute": {"future": {"other": "{0}분 후"}, "past": {"other": "{0}분 전"}}, + "second": {"future": {"other": "{0}초 후"}, "past": {"other": "{0}초 전"}}, + }, + "day_periods": { + "midnight": "자정", + "am": "오전", + "noon": "정오", + "pm": "오후", + "morning1": "새벽", + "morning2": "오전", + "afternoon1": "오후", + "evening1": "저녁", + "night1": "밤", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/locale.py b/src/pendulum/locales/locale.py new file mode 100644 index 0000000..21eaaec --- /dev/null +++ b/src/pendulum/locales/locale.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +import re + +from importlib import import_module +from pathlib import Path +from typing import Any +from typing import ClassVar +from typing import Dict +from typing import cast + +from pendulum.utils._compat import resources + + +class Locale: + """ + Represent a specific locale. + """ + + _cache: ClassVar[dict[str, Locale]] = {} + + def __init__(self, locale: str, data: Any) -> None: + self._locale: str = locale + self._data: Any = data + self._key_cache: dict[str, str] = {} + + @classmethod + def load(cls, locale: str | Locale) -> Locale: + if isinstance(locale, Locale): + return locale + + locale = cls.normalize_locale(locale) + if locale in cls._cache: + return cls._cache[locale] + + # Checking locale existence + actual_locale = locale + locale_path = cast(Path, resources.files(__package__).joinpath(actual_locale)) + while not locale_path.exists(): + if actual_locale == locale: + raise ValueError(f"Locale [{locale}] does not exist.") + + actual_locale = actual_locale.split("_")[0] + + m = import_module(f"pendulum.locales.{actual_locale}.locale") + + cls._cache[locale] = cls(locale, m.locale) + + return cls._cache[locale] + + @classmethod + def normalize_locale(cls, locale: str) -> str: + m = re.match("([a-z]{2})[-_]([a-z]{2})", locale, re.I) + if m: + return f"{m.group(1).lower()}_{m.group(2).lower()}" + else: + return locale.lower() + + def get(self, key: str, default: Any | None = None) -> Any: + if key in self._key_cache: + return self._key_cache[key] + + parts = key.split(".") + try: + result = self._data[parts[0]] + for part in parts[1:]: + result = result[part] + except KeyError: + result = default + + self._key_cache[key] = result + + return self._key_cache[key] + + def translation(self, key: str) -> Any: + return self.get(f"translations.{key}") + + def plural(self, number: int) -> str: + return cast(str, self._data["plural"](number)) + + def ordinal(self, number: int) -> str: + return cast(str, self._data["ordinal"](number)) + + def ordinalize(self, number: int) -> str: + ordinal = self.get(f"custom.ordinal.{self.ordinal(number)}") + + if not ordinal: + return f"{number}" + + return f"{number}{ordinal}" + + def match_translation(self, key: str, value: Any) -> dict[str, str] | None: + translations = self.translation(key) + if value not in translations.values(): + return None + + return cast(Dict[str, str], {v: k for k, v in translations.items()}[value]) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}('{self._locale}')" diff --git a/src/pendulum/locales/lt/__init__.py b/src/pendulum/locales/lt/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/lt/__init__.py diff --git a/src/pendulum/locales/lt/custom.py b/src/pendulum/locales/lt/custom.py new file mode 100644 index 0000000..d7f17d3 --- /dev/null +++ b/src/pendulum/locales/lt/custom.py @@ -0,0 +1,120 @@ +""" +lt custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "units_relative": { + "year": { + "future": { + "other": "{0} metų", + "one": "{0} metų", + "few": "{0} metų", + "many": "{0} metų", + }, + "past": { + "other": "{0} metų", + "one": "{0} metus", + "few": "{0} metus", + "many": "{0} metų", + }, + }, + "month": { + "future": { + "other": "{0} mėnesių", + "one": "{0} mėnesio", + "few": "{0} mėnesių", + "many": "{0} mėnesio", + }, + "past": { + "other": "{0} mėnesių", + "one": "{0} mėnesį", + "few": "{0} mėnesius", + "many": "{0} mėnesio", + }, + }, + "week": { + "future": { + "other": "{0} savaičių", + "one": "{0} savaitės", + "few": "{0} savaičių", + "many": "{0} savaitės", + }, + "past": { + "other": "{0} savaičių", + "one": "{0} savaitę", + "few": "{0} savaites", + "many": "{0} savaitės", + }, + }, + "day": { + "future": { + "other": "{0} dienų", + "one": "{0} dienos", + "few": "{0} dienų", + "many": "{0} dienos", + }, + "past": { + "other": "{0} dienų", + "one": "{0} dieną", + "few": "{0} dienas", + "many": "{0} dienos", + }, + }, + "hour": { + "future": { + "other": "{0} valandų", + "one": "{0} valandos", + "few": "{0} valandų", + "many": "{0} valandos", + }, + "past": { + "other": "{0} valandų", + "one": "{0} valandą", + "few": "{0} valandas", + "many": "{0} valandos", + }, + }, + "minute": { + "future": { + "other": "{0} minučių", + "one": "{0} minutės", + "few": "{0} minučių", + "many": "{0} minutės", + }, + "past": { + "other": "{0} minučių", + "one": "{0} minutę", + "few": "{0} minutes", + "many": "{0} minutės", + }, + }, + "second": { + "future": { + "other": "{0} sekundžių", + "one": "{0} sekundės", + "few": "{0} sekundžių", + "many": "{0} sekundės", + }, + "past": { + "other": "{0} sekundžių", + "one": "{0} sekundę", + "few": "{0} sekundes", + "many": "{0} sekundės", + }, + }, + }, + "after": "po {0}", + "before": "{0} nuo dabar", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "YYYY [m.] MMMM D [d.], dddd, HH:mm [val.]", + "LLL": "YYYY [m.] MMMM D [d.], HH:mm [val.]", + "LL": "YYYY [m.] MMMM D [d.]", + "L": "YYYY-MM-DD", + }, +} diff --git a/src/pendulum/locales/lt/locale.py b/src/pendulum/locales/lt/locale.py new file mode 100644 index 0000000..6e9a460 --- /dev/null +++ b/src/pendulum/locales/lt/locale.py @@ -0,0 +1,263 @@ +from __future__ import annotations + +from pendulum.locales.lt.custom import translations as custom_translations + + +""" +lt locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "few" + if ( + ((n % 10) == (n % 10) and ((n % 10) >= 2 and (n % 10) <= 9)) + and (not ((n % 100) == (n % 100) and ((n % 100) >= 11 and (n % 100) <= 19))) + ) + else "many" + if (not (0 == 0 and (0 == 0))) + else "one" + if ( + ((n % 10) == (n % 10) and ((n % 10) == 1)) + and (not ((n % 100) == (n % 100) and ((n % 100) >= 11 and (n % 100) <= 19))) + ) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "pr", + 1: "an", + 2: "tr", + 3: "kt", + 4: "pn", + 5: "št", + 6: "sk", + }, + "narrow": {0: "P", 1: "A", 2: "T", 3: "K", 4: "P", 5: "Š", 6: "S"}, + "short": {0: "Pr", 1: "An", 2: "Tr", 3: "Kt", 4: "Pn", 5: "Št", 6: "Sk"}, + "wide": { + 0: "pirmadienis", + 1: "antradienis", + 2: "trečiadienis", + 3: "ketvirtadienis", + 4: "penktadienis", + 5: "šeštadienis", + 6: "sekmadienis", + }, + }, + "months": { + "abbreviated": { + 1: "saus.", + 2: "vas.", + 3: "kov.", + 4: "bal.", + 5: "geg.", + 6: "birž.", + 7: "liep.", + 8: "rugp.", + 9: "rugs.", + 10: "spal.", + 11: "lapkr.", + 12: "gruod.", + }, + "narrow": { + 1: "S", + 2: "V", + 3: "K", + 4: "B", + 5: "G", + 6: "B", + 7: "L", + 8: "R", + 9: "R", + 10: "S", + 11: "L", + 12: "G", + }, + "wide": { + 1: "sausio", + 2: "vasario", + 3: "kovo", + 4: "balandžio", + 5: "gegužės", + 6: "birželio", + 7: "liepos", + 8: "rugpjūčio", + 9: "rugsėjo", + 10: "spalio", + 11: "lapkričio", + 12: "gruodžio", + }, + }, + "units": { + "year": { + "one": "{0} metai", + "few": "{0} metai", + "many": "{0} metų", + "other": "{0} metų", + }, + "month": { + "one": "{0} mėnuo", + "few": "{0} mėnesiai", + "many": "{0} mėnesio", + "other": "{0} mėnesių", + }, + "week": { + "one": "{0} savaitė", + "few": "{0} savaitės", + "many": "{0} savaitės", + "other": "{0} savaičių", + }, + "day": { + "one": "{0} diena", + "few": "{0} dienos", + "many": "{0} dienos", + "other": "{0} dienų", + }, + "hour": { + "one": "{0} valanda", + "few": "{0} valandos", + "many": "{0} valandos", + "other": "{0} valandų", + }, + "minute": { + "one": "{0} minutė", + "few": "{0} minutės", + "many": "{0} minutės", + "other": "{0} minučių", + }, + "second": { + "one": "{0} sekundė", + "few": "{0} sekundės", + "many": "{0} sekundės", + "other": "{0} sekundžių", + }, + "microsecond": { + "one": "{0} mikrosekundė", + "few": "{0} mikrosekundės", + "many": "{0} mikrosekundės", + "other": "{0} mikrosekundžių", + }, + }, + "relative": { + "year": { + "future": { + "other": "po {0} metų", + "one": "po {0} metų", + "few": "po {0} metų", + "many": "po {0} metų", + }, + "past": { + "other": "prieš {0} metų", + "one": "prieš {0} metus", + "few": "prieš {0} metus", + "many": "prieš {0} metų", + }, + }, + "month": { + "future": { + "other": "po {0} mėnesių", + "one": "po {0} mėnesio", + "few": "po {0} mėnesių", + "many": "po {0} mėnesio", + }, + "past": { + "other": "prieš {0} mėnesių", + "one": "prieš {0} mėnesį", + "few": "prieš {0} mėnesius", + "many": "prieš {0} mėnesio", + }, + }, + "week": { + "future": { + "other": "po {0} savaičių", + "one": "po {0} savaitės", + "few": "po {0} savaičių", + "many": "po {0} savaitės", + }, + "past": { + "other": "prieš {0} savaičių", + "one": "prieš {0} savaitę", + "few": "prieš {0} savaites", + "many": "prieš {0} savaitės", + }, + }, + "day": { + "future": { + "other": "po {0} dienų", + "one": "po {0} dienos", + "few": "po {0} dienų", + "many": "po {0} dienos", + }, + "past": { + "other": "prieš {0} dienų", + "one": "prieš {0} dieną", + "few": "prieš {0} dienas", + "many": "prieš {0} dienos", + }, + }, + "hour": { + "future": { + "other": "po {0} valandų", + "one": "po {0} valandos", + "few": "po {0} valandų", + "many": "po {0} valandos", + }, + "past": { + "other": "prieš {0} valandų", + "one": "prieš {0} valandą", + "few": "prieš {0} valandas", + "many": "prieš {0} valandos", + }, + }, + "minute": { + "future": { + "other": "po {0} minučių", + "one": "po {0} minutės", + "few": "po {0} minučių", + "many": "po {0} minutės", + }, + "past": { + "other": "prieš {0} minučių", + "one": "prieš {0} minutę", + "few": "prieš {0} minutes", + "many": "prieš {0} minutės", + }, + }, + "second": { + "future": { + "other": "po {0} sekundžių", + "one": "po {0} sekundės", + "few": "po {0} sekundžių", + "many": "po {0} sekundės", + }, + "past": { + "other": "prieš {0} sekundžių", + "one": "prieš {0} sekundę", + "few": "prieš {0} sekundes", + "many": "prieš {0} sekundės", + }, + }, + }, + "day_periods": { + "midnight": "vidurnaktis", + "am": "priešpiet", + "noon": "perpiet", + "pm": "popiet", + "morning1": "rytas", + "afternoon1": "popietė", + "evening1": "vakaras", + "night1": "naktis", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/nb/__init__.py b/src/pendulum/locales/nb/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/nb/__init__.py diff --git a/src/pendulum/locales/nb/custom.py b/src/pendulum/locales/nb/custom.py new file mode 100644 index 0000000..554e7f3 --- /dev/null +++ b/src/pendulum/locales/nb/custom.py @@ -0,0 +1,22 @@ +""" +nn custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "after": "{0} etter", + "before": "{0} før", + # Ordinals + "ordinal": {"one": ".", "two": ".", "few": ".", "other": "."}, + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd Do MMMM YYYY HH:mm", + "LLL": "Do MMMM YYYY HH:mm", + "LL": "Do MMMM YYYY", + "L": "DD.MM.YYYY", + }, +} diff --git a/src/pendulum/locales/nb/locale.py b/src/pendulum/locales/nb/locale.py new file mode 100644 index 0000000..084f019 --- /dev/null +++ b/src/pendulum/locales/nb/locale.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +from pendulum.locales.nb.custom import translations as custom_translations + + +""" +nb locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" if (n == n and (n == 1)) else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "man.", + 1: "tir.", + 2: "ons.", + 3: "tor.", + 4: "fre.", + 5: "lør.", + 6: "søn.", + }, + "narrow": {0: "M", 1: "T", 2: "O", 3: "T", 4: "F", 5: "L", 6: "S"}, + "short": { + 0: "ma.", + 1: "ti.", + 2: "on.", + 3: "to.", + 4: "fr.", + 5: "lø.", + 6: "sø.", + }, + "wide": { + 0: "mandag", + 1: "tirsdag", + 2: "onsdag", + 3: "torsdag", + 4: "fredag", + 5: "lørdag", + 6: "søndag", + }, + }, + "months": { + "abbreviated": { + 1: "jan.", + 2: "feb.", + 3: "mar.", + 4: "apr.", + 5: "mai", + 6: "jun.", + 7: "jul.", + 8: "aug.", + 9: "sep.", + 10: "okt.", + 11: "nov.", + 12: "des.", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "januar", + 2: "februar", + 3: "mars", + 4: "april", + 5: "mai", + 6: "juni", + 7: "juli", + 8: "august", + 9: "september", + 10: "oktober", + 11: "november", + 12: "desember", + }, + }, + "units": { + "year": {"one": "{0} år", "other": "{0} år"}, + "month": {"one": "{0} måned", "other": "{0} måneder"}, + "week": {"one": "{0} uke", "other": "{0} uker"}, + "day": {"one": "{0} dag", "other": "{0} dager"}, + "hour": {"one": "{0} time", "other": "{0} timer"}, + "minute": {"one": "{0} minutt", "other": "{0} minutter"}, + "second": {"one": "{0} sekund", "other": "{0} sekunder"}, + "microsecond": {"one": "{0} mikrosekund", "other": "{0} mikrosekunder"}, + }, + "relative": { + "year": { + "future": {"other": "om {0} år", "one": "om {0} år"}, + "past": {"other": "for {0} år siden", "one": "for {0} år siden"}, + }, + "month": { + "future": {"other": "om {0} måneder", "one": "om {0} måned"}, + "past": { + "other": "for {0} måneder siden", + "one": "for {0} måned siden", + }, + }, + "week": { + "future": {"other": "om {0} uker", "one": "om {0} uke"}, + "past": {"other": "for {0} uker siden", "one": "for {0} uke siden"}, + }, + "day": { + "future": {"other": "om {0} dager", "one": "om {0} dag"}, + "past": {"other": "for {0} dager siden", "one": "for {0} dag siden"}, + }, + "hour": { + "future": {"other": "om {0} timer", "one": "om {0} time"}, + "past": {"other": "for {0} timer siden", "one": "for {0} time siden"}, + }, + "minute": { + "future": {"other": "om {0} minutter", "one": "om {0} minutt"}, + "past": { + "other": "for {0} minutter siden", + "one": "for {0} minutt siden", + }, + }, + "second": { + "future": {"other": "om {0} sekunder", "one": "om {0} sekund"}, + "past": { + "other": "for {0} sekunder siden", + "one": "for {0} sekund siden", + }, + }, + }, + "day_periods": { + "midnight": "midnatt", + "am": "a.m.", + "pm": "p.m.", + "morning1": "morgenen", + "morning2": "formiddagen", + "afternoon1": "ettermiddagen", + "evening1": "kvelden", + "night1": "natten", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/nl/__init__.py b/src/pendulum/locales/nl/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/nl/__init__.py diff --git a/src/pendulum/locales/nl/custom.py b/src/pendulum/locales/nl/custom.py new file mode 100644 index 0000000..ca90673 --- /dev/null +++ b/src/pendulum/locales/nl/custom.py @@ -0,0 +1,25 @@ +""" +nl custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "enkele seconden"}, + # Relative time + "ago": "{} geleden", + "from_now": "over {}", + "after": "{0} later", + "before": "{0} eerder", + # Ordinals + "ordinal": {"other": "e"}, + # Date formats + "date_formats": { + "L": "DD-MM-YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY HH:mm", + "LLLL": "dddd D MMMM YYYY HH:mm", + "LT": "HH:mm", + "LTS": "HH:mm:ss", + }, +} diff --git a/src/pendulum/locales/nl/locale.py b/src/pendulum/locales/nl/locale.py new file mode 100644 index 0000000..68b54e7 --- /dev/null +++ b/src/pendulum/locales/nl/locale.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +from pendulum.locales.nl.custom import translations as custom_translations + + +""" +nl locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "ma", + 1: "di", + 2: "wo", + 3: "do", + 4: "vr", + 5: "za", + 6: "zo", + }, + "narrow": {0: "M", 1: "D", 2: "W", 3: "D", 4: "V", 5: "Z", 6: "Z"}, + "short": {0: "ma", 1: "di", 2: "wo", 3: "do", 4: "vr", 5: "za", 6: "zo"}, + "wide": { + 0: "maandag", + 1: "dinsdag", + 2: "woensdag", + 3: "donderdag", + 4: "vrijdag", + 5: "zaterdag", + 6: "zondag", + }, + }, + "months": { + "abbreviated": { + 1: "jan.", + 2: "feb.", + 3: "mrt.", + 4: "apr.", + 5: "mei", + 6: "jun.", + 7: "jul.", + 8: "aug.", + 9: "sep.", + 10: "okt.", + 11: "nov.", + 12: "dec.", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "januari", + 2: "februari", + 3: "maart", + 4: "april", + 5: "mei", + 6: "juni", + 7: "juli", + 8: "augustus", + 9: "september", + 10: "oktober", + 11: "november", + 12: "december", + }, + }, + "units": { + "year": {"one": "{0} jaar", "other": "{0} jaar"}, + "month": {"one": "{0} maand", "other": "{0} maanden"}, + "week": {"one": "{0} week", "other": "{0} weken"}, + "day": {"one": "{0} dag", "other": "{0} dagen"}, + "hour": {"one": "{0} uur", "other": "{0} uur"}, + "minute": {"one": "{0} minuut", "other": "{0} minuten"}, + "second": {"one": "{0} seconde", "other": "{0} seconden"}, + "microsecond": {"one": "{0} microseconde", "other": "{0} microseconden"}, + }, + "relative": { + "year": { + "future": {"other": "over {0} jaar", "one": "over {0} jaar"}, + "past": {"other": "{0} jaar geleden", "one": "{0} jaar geleden"}, + }, + "month": { + "future": {"other": "over {0} maanden", "one": "over {0} maand"}, + "past": {"other": "{0} maanden geleden", "one": "{0} maand geleden"}, + }, + "week": { + "future": {"other": "over {0} weken", "one": "over {0} week"}, + "past": {"other": "{0} weken geleden", "one": "{0} week geleden"}, + }, + "day": { + "future": {"other": "over {0} dagen", "one": "over {0} dag"}, + "past": {"other": "{0} dagen geleden", "one": "{0} dag geleden"}, + }, + "hour": { + "future": {"other": "over {0} uur", "one": "over {0} uur"}, + "past": {"other": "{0} uur geleden", "one": "{0} uur geleden"}, + }, + "minute": { + "future": {"other": "over {0} minuten", "one": "over {0} minuut"}, + "past": {"other": "{0} minuten geleden", "one": "{0} minuut geleden"}, + }, + "second": { + "future": {"other": "over {0} seconden", "one": "over {0} seconde"}, + "past": {"other": "{0} seconden geleden", "one": "{0} seconde geleden"}, + }, + }, + "day_periods": { + "midnight": "middernacht", + "am": "a.m.", + "pm": "p.m.", + "morning1": "‘s ochtends", + "afternoon1": "‘s middags", + "evening1": "‘s avonds", + "night1": "‘s nachts", + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/nn/__init__.py b/src/pendulum/locales/nn/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/nn/__init__.py diff --git a/src/pendulum/locales/nn/custom.py b/src/pendulum/locales/nn/custom.py new file mode 100644 index 0000000..554e7f3 --- /dev/null +++ b/src/pendulum/locales/nn/custom.py @@ -0,0 +1,22 @@ +""" +nn custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "after": "{0} etter", + "before": "{0} før", + # Ordinals + "ordinal": {"one": ".", "two": ".", "few": ".", "other": "."}, + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd Do MMMM YYYY HH:mm", + "LLL": "Do MMMM YYYY HH:mm", + "LL": "Do MMMM YYYY", + "L": "DD.MM.YYYY", + }, +} diff --git a/src/pendulum/locales/nn/locale.py b/src/pendulum/locales/nn/locale.py new file mode 100644 index 0000000..737ee6d --- /dev/null +++ b/src/pendulum/locales/nn/locale.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +from pendulum.locales.nn.custom import translations as custom_translations + + +""" +nn locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" if (n == n and (n == 1)) else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "mån.", + 1: "tys.", + 2: "ons.", + 3: "tor.", + 4: "fre.", + 5: "lau.", + 6: "søn.", + }, + "narrow": {0: "M", 1: "T", 2: "O", 3: "T", 4: "F", 5: "L", 6: "S"}, + "short": { + 0: "må.", + 1: "ty.", + 2: "on.", + 3: "to.", + 4: "fr.", + 5: "la.", + 6: "sø.", + }, + "wide": { + 0: "måndag", + 1: "tysdag", + 2: "onsdag", + 3: "torsdag", + 4: "fredag", + 5: "laurdag", + 6: "søndag", + }, + }, + "months": { + "abbreviated": { + 1: "jan.", + 2: "feb.", + 3: "mars", + 4: "apr.", + 5: "mai", + 6: "juni", + 7: "juli", + 8: "aug.", + 9: "sep.", + 10: "okt.", + 11: "nov.", + 12: "des.", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "januar", + 2: "februar", + 3: "mars", + 4: "april", + 5: "mai", + 6: "juni", + 7: "juli", + 8: "august", + 9: "september", + 10: "oktober", + 11: "november", + 12: "desember", + }, + }, + "units": { + "year": {"one": "{0} år", "other": "{0} år"}, + "month": {"one": "{0} månad", "other": "{0} månadar"}, + "week": {"one": "{0} veke", "other": "{0} veker"}, + "day": {"one": "{0} dag", "other": "{0} dagar"}, + "hour": {"one": "{0} time", "other": "{0} timar"}, + "minute": {"one": "{0} minutt", "other": "{0} minutt"}, + "second": {"one": "{0} sekund", "other": "{0} sekund"}, + "microsecond": {"one": "{0} mikrosekund", "other": "{0} mikrosekund"}, + }, + "relative": { + "year": { + "future": {"other": "om {0} år", "one": "om {0} år"}, + "past": {"other": "for {0} år sidan", "one": "for {0} år sidan"}, + }, + "month": { + "future": {"other": "om {0} månadar", "one": "om {0} månad"}, + "past": { + "other": "for {0} månadar sidan", + "one": "for {0} månad sidan", + }, + }, + "week": { + "future": {"other": "om {0} veker", "one": "om {0} veke"}, + "past": {"other": "for {0} veker sidan", "one": "for {0} veke sidan"}, + }, + "day": { + "future": {"other": "om {0} dagar", "one": "om {0} dag"}, + "past": {"other": "for {0} dagar sidan", "one": "for {0} dag sidan"}, + }, + "hour": { + "future": {"other": "om {0} timar", "one": "om {0} time"}, + "past": {"other": "for {0} timar sidan", "one": "for {0} time sidan"}, + }, + "minute": { + "future": {"other": "om {0} minutt", "one": "om {0} minutt"}, + "past": { + "other": "for {0} minutt sidan", + "one": "for {0} minutt sidan", + }, + }, + "second": { + "future": {"other": "om {0} sekund", "one": "om {0} sekund"}, + "past": { + "other": "for {0} sekund sidan", + "one": "for {0} sekund sidan", + }, + }, + }, + "day_periods": {"am": "formiddag", "pm": "ettermiddag"}, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/pl/__init__.py b/src/pendulum/locales/pl/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/pl/__init__.py diff --git a/src/pendulum/locales/pl/custom.py b/src/pendulum/locales/pl/custom.py new file mode 100644 index 0000000..0aaab90 --- /dev/null +++ b/src/pendulum/locales/pl/custom.py @@ -0,0 +1,23 @@ +""" +pl custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "kilka sekund"}, + # Relative time + "ago": "{} temu", + "from_now": "za {}", + "after": "{0} po", + "before": "{0} przed", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "L": "DD.MM.YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY HH:mm", + "LLLL": "dddd, D MMMM YYYY HH:mm", + }, +} diff --git a/src/pendulum/locales/pl/locale.py b/src/pendulum/locales/pl/locale.py new file mode 100644 index 0000000..c709120 --- /dev/null +++ b/src/pendulum/locales/pl/locale.py @@ -0,0 +1,287 @@ +from __future__ import annotations + +from pendulum.locales.pl.custom import translations as custom_translations + + +""" +pl locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "few" + if ( + ( + (0 == 0 and (0 == 0)) + and ((n % 10) == (n % 10) and ((n % 10) >= 2 and (n % 10) <= 4)) + ) + and (not ((n % 100) == (n % 100) and ((n % 100) >= 12 and (n % 100) <= 14))) + ) + else "many" + if ( + ( + ( + ((0 == 0 and (0 == 0)) and (not (n == n and (n == 1)))) + and ((n % 10) == (n % 10) and ((n % 10) >= 0 and (n % 10) <= 1)) + ) + or ( + (0 == 0 and (0 == 0)) + and ((n % 10) == (n % 10) and ((n % 10) >= 5 and (n % 10) <= 9)) + ) + ) + or ( + (0 == 0 and (0 == 0)) + and ((n % 100) == (n % 100) and ((n % 100) >= 12 and (n % 100) <= 14)) + ) + ) + else "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "pon.", + 1: "wt.", + 2: "śr.", + 3: "czw.", + 4: "pt.", + 5: "sob.", + 6: "niedz.", + }, + "narrow": {0: "p", 1: "w", 2: "ś", 3: "c", 4: "p", 5: "s", 6: "n"}, + "short": { + 0: "pon", + 1: "wto", + 2: "śro", + 3: "czw", + 4: "pią", + 5: "sob", + 6: "nie", + }, + "wide": { + 0: "poniedziałek", + 1: "wtorek", + 2: "środa", + 3: "czwartek", + 4: "piątek", + 5: "sobota", + 6: "niedziela", + }, + }, + "months": { + "abbreviated": { + 1: "sty", + 2: "lut", + 3: "mar", + 4: "kwi", + 5: "maj", + 6: "cze", + 7: "lip", + 8: "sie", + 9: "wrz", + 10: "paź", + 11: "lis", + 12: "gru", + }, + "narrow": { + 1: "s", + 2: "l", + 3: "m", + 4: "k", + 5: "m", + 6: "c", + 7: "l", + 8: "s", + 9: "w", + 10: "p", + 11: "l", + 12: "g", + }, + "wide": { + 1: "stycznia", + 2: "lutego", + 3: "marca", + 4: "kwietnia", + 5: "maja", + 6: "czerwca", + 7: "lipca", + 8: "sierpnia", + 9: "września", + 10: "października", + 11: "listopada", + 12: "grudnia", + }, + }, + "units": { + "year": { + "one": "{0} rok", + "few": "{0} lata", + "many": "{0} lat", + "other": "{0} roku", + }, + "month": { + "one": "{0} miesiąc", + "few": "{0} miesiące", + "many": "{0} miesięcy", + "other": "{0} miesiąca", + }, + "week": { + "one": "{0} tydzień", + "few": "{0} tygodnie", + "many": "{0} tygodni", + "other": "{0} tygodnia", + }, + "day": { + "one": "{0} dzień", + "few": "{0} dni", + "many": "{0} dni", + "other": "{0} dnia", + }, + "hour": { + "one": "{0} godzina", + "few": "{0} godziny", + "many": "{0} godzin", + "other": "{0} godziny", + }, + "minute": { + "one": "{0} minuta", + "few": "{0} minuty", + "many": "{0} minut", + "other": "{0} minuty", + }, + "second": { + "one": "{0} sekunda", + "few": "{0} sekundy", + "many": "{0} sekund", + "other": "{0} sekundy", + }, + "microsecond": { + "one": "{0} mikrosekunda", + "few": "{0} mikrosekundy", + "many": "{0} mikrosekund", + "other": "{0} mikrosekundy", + }, + }, + "relative": { + "year": { + "future": { + "other": "za {0} roku", + "one": "za {0} rok", + "few": "za {0} lata", + "many": "za {0} lat", + }, + "past": { + "other": "{0} roku temu", + "one": "{0} rok temu", + "few": "{0} lata temu", + "many": "{0} lat temu", + }, + }, + "month": { + "future": { + "other": "za {0} miesiąca", + "one": "za {0} miesiąc", + "few": "za {0} miesiące", + "many": "za {0} miesięcy", + }, + "past": { + "other": "{0} miesiąca temu", + "one": "{0} miesiąc temu", + "few": "{0} miesiące temu", + "many": "{0} miesięcy temu", + }, + }, + "week": { + "future": { + "other": "za {0} tygodnia", + "one": "za {0} tydzień", + "few": "za {0} tygodnie", + "many": "za {0} tygodni", + }, + "past": { + "other": "{0} tygodnia temu", + "one": "{0} tydzień temu", + "few": "{0} tygodnie temu", + "many": "{0} tygodni temu", + }, + }, + "day": { + "future": { + "other": "za {0} dnia", + "one": "za {0} dzień", + "few": "za {0} dni", + "many": "za {0} dni", + }, + "past": { + "other": "{0} dnia temu", + "one": "{0} dzień temu", + "few": "{0} dni temu", + "many": "{0} dni temu", + }, + }, + "hour": { + "future": { + "other": "za {0} godziny", + "one": "za {0} godzinę", + "few": "za {0} godziny", + "many": "za {0} godzin", + }, + "past": { + "other": "{0} godziny temu", + "one": "{0} godzinę temu", + "few": "{0} godziny temu", + "many": "{0} godzin temu", + }, + }, + "minute": { + "future": { + "other": "za {0} minuty", + "one": "za {0} minutę", + "few": "za {0} minuty", + "many": "za {0} minut", + }, + "past": { + "other": "{0} minuty temu", + "one": "{0} minutę temu", + "few": "{0} minuty temu", + "many": "{0} minut temu", + }, + }, + "second": { + "future": { + "other": "za {0} sekundy", + "one": "za {0} sekundę", + "few": "za {0} sekundy", + "many": "za {0} sekund", + }, + "past": { + "other": "{0} sekundy temu", + "one": "{0} sekundę temu", + "few": "{0} sekundy temu", + "many": "{0} sekund temu", + }, + }, + }, + "day_periods": { + "midnight": "o północy", + "am": "AM", + "noon": "w południe", + "pm": "PM", + "morning1": "rano", + "morning2": "przed południem", + "afternoon1": "po południu", + "evening1": "wieczorem", + "night1": "w nocy", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/pt_br/__init__.py b/src/pendulum/locales/pt_br/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/pt_br/__init__.py diff --git a/src/pendulum/locales/pt_br/custom.py b/src/pendulum/locales/pt_br/custom.py new file mode 100644 index 0000000..87a7702 --- /dev/null +++ b/src/pendulum/locales/pt_br/custom.py @@ -0,0 +1,20 @@ +""" +pt-br custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "after": "após {0}", + "before": "{0} atrás", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd, D [de] MMMM [de] YYYY [às] HH:mm", + "LLL": "D [de] MMMM [de] YYYY [às] HH:mm", + "LL": "D [de] MMMM [de] YYYY", + "L": "DD/MM/YYYY", + }, +} diff --git a/src/pendulum/locales/pt_br/locale.py b/src/pendulum/locales/pt_br/locale.py new file mode 100644 index 0000000..793cba8 --- /dev/null +++ b/src/pendulum/locales/pt_br/locale.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +from pendulum.locales.pt_br.custom import translations as custom_translations + + +""" +pt_br locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and (n >= 0 and n <= 2)) and (not (n == n and (n == 2)))) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "seg", + 1: "ter", + 2: "qua", + 3: "qui", + 4: "sex", + 5: "sáb", + 6: "dom", + }, + "narrow": {0: "S", 1: "T", 2: "Q", 3: "Q", 4: "S", 5: "S", 6: "D"}, + "short": { + 0: "seg", + 1: "ter", + 2: "qua", + 3: "qui", + 4: "sex", + 5: "sáb", + 6: "dom", + }, + "wide": { + 0: "segunda-feira", + 1: "terça-feira", + 2: "quarta-feira", + 3: "quinta-feira", + 4: "sexta-feira", + 5: "sábado", + 6: "domingo", + }, + }, + "months": { + "abbreviated": { + 1: "jan", + 2: "fev", + 3: "mar", + 4: "abr", + 5: "mai", + 6: "jun", + 7: "jul", + 8: "ago", + 9: "set", + 10: "out", + 11: "nov", + 12: "dez", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "janeiro", + 2: "fevereiro", + 3: "março", + 4: "abril", + 5: "maio", + 6: "junho", + 7: "julho", + 8: "agosto", + 9: "setembro", + 10: "outubro", + 11: "novembro", + 12: "dezembro", + }, + }, + "units": { + "year": {"one": "{0} ano", "other": "{0} anos"}, + "month": {"one": "{0} mês", "other": "{0} meses"}, + "week": {"one": "{0} semana", "other": "{0} semanas"}, + "day": {"one": "{0} dia", "other": "{0} dias"}, + "hour": {"one": "{0} hora", "other": "{0} horas"}, + "minute": {"one": "{0} minuto", "other": "{0} minutos"}, + "second": {"one": "{0} segundo", "other": "{0} segundos"}, + "microsecond": {"one": "{0} microssegundo", "other": "{0} microssegundos"}, + }, + "relative": { + "year": { + "future": {"other": "em {0} anos", "one": "em {0} ano"}, + "past": {"other": "há {0} anos", "one": "há {0} ano"}, + }, + "month": { + "future": {"other": "em {0} meses", "one": "em {0} mês"}, + "past": {"other": "há {0} meses", "one": "há {0} mês"}, + }, + "week": { + "future": {"other": "em {0} semanas", "one": "em {0} semana"}, + "past": {"other": "há {0} semanas", "one": "há {0} semana"}, + }, + "day": { + "future": {"other": "em {0} dias", "one": "em {0} dia"}, + "past": {"other": "há {0} dias", "one": "há {0} dia"}, + }, + "hour": { + "future": {"other": "em {0} horas", "one": "em {0} hora"}, + "past": {"other": "há {0} horas", "one": "há {0} hora"}, + }, + "minute": { + "future": {"other": "em {0} minutos", "one": "em {0} minuto"}, + "past": {"other": "há {0} minutos", "one": "há {0} minuto"}, + }, + "second": { + "future": {"other": "em {0} segundos", "one": "em {0} segundo"}, + "past": {"other": "há {0} segundos", "one": "há {0} segundo"}, + }, + }, + "day_periods": { + "midnight": "meia-noite", + "am": "AM", + "noon": "meio-dia", + "pm": "PM", + "morning1": "da manhã", + "afternoon1": "da tarde", + "evening1": "da noite", + "night1": "da madrugada", + }, + "week_data": { + "min_days": 1, + "first_day": 6, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/ru/__init__.py b/src/pendulum/locales/ru/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/ru/__init__.py diff --git a/src/pendulum/locales/ru/custom.py b/src/pendulum/locales/ru/custom.py new file mode 100644 index 0000000..e1f87ff --- /dev/null +++ b/src/pendulum/locales/ru/custom.py @@ -0,0 +1,22 @@ +""" +ru custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "ago": "{} назад", + "from_now": "через {}", + "after": "{0} после", + "before": "{0} до", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "L": "DD.MM.YYYY", + "LL": "D MMMM YYYY г.", + "LLL": "D MMMM YYYY г., HH:mm", + "LLLL": "dddd, D MMMM YYYY г., HH:mm", + }, +} diff --git a/src/pendulum/locales/ru/locale.py b/src/pendulum/locales/ru/locale.py new file mode 100644 index 0000000..b9eab83 --- /dev/null +++ b/src/pendulum/locales/ru/locale.py @@ -0,0 +1,278 @@ +from __future__ import annotations + +from pendulum.locales.ru.custom import translations as custom_translations + + +""" +ru locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "few" + if ( + ( + (0 == 0 and (0 == 0)) + and ((n % 10) == (n % 10) and ((n % 10) >= 2 and (n % 10) <= 4)) + ) + and (not ((n % 100) == (n % 100) and ((n % 100) >= 12 and (n % 100) <= 14))) + ) + else "many" + if ( + ( + ((0 == 0 and (0 == 0)) and ((n % 10) == (n % 10) and ((n % 10) == 0))) + or ( + (0 == 0 and (0 == 0)) + and ((n % 10) == (n % 10) and ((n % 10) >= 5 and (n % 10) <= 9)) + ) + ) + or ( + (0 == 0 and (0 == 0)) + and ((n % 100) == (n % 100) and ((n % 100) >= 11 and (n % 100) <= 14)) + ) + ) + else "one" + if ( + ((0 == 0 and (0 == 0)) and ((n % 10) == (n % 10) and ((n % 10) == 1))) + and (not ((n % 100) == (n % 100) and ((n % 100) == 11))) + ) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "пн", + 1: "вт", + 2: "ср", + 3: "чт", + 4: "пт", + 5: "сб", + 6: "вс", + }, + "narrow": {0: "пн", 1: "вт", 2: "ср", 3: "чт", 4: "пт", 5: "сб", 6: "вс"}, + "short": {0: "пн", 1: "вт", 2: "ср", 3: "чт", 4: "пт", 5: "сб", 6: "вс"}, + "wide": { + 0: "понедельник", + 1: "вторник", + 2: "среда", + 3: "четверг", + 4: "пятница", + 5: "суббота", + 6: "воскресенье", + }, + }, + "months": { + "abbreviated": { + 1: "янв.", + 2: "февр.", + 3: "мар.", + 4: "апр.", + 5: "мая", + 6: "июн.", + 7: "июл.", + 8: "авг.", + 9: "сент.", + 10: "окт.", + 11: "нояб.", + 12: "дек.", + }, + "narrow": { + 1: "Я", + 2: "Ф", + 3: "М", + 4: "А", + 5: "М", + 6: "И", + 7: "И", + 8: "А", + 9: "С", + 10: "О", + 11: "Н", + 12: "Д", + }, + "wide": { + 1: "января", + 2: "февраля", + 3: "марта", + 4: "апреля", + 5: "мая", + 6: "июня", + 7: "июля", + 8: "августа", + 9: "сентября", + 10: "октября", + 11: "ноября", + 12: "декабря", + }, + }, + "units": { + "year": { + "one": "{0} год", + "few": "{0} года", + "many": "{0} лет", + "other": "{0} года", + }, + "month": { + "one": "{0} месяц", + "few": "{0} месяца", + "many": "{0} месяцев", + "other": "{0} месяца", + }, + "week": { + "one": "{0} неделя", + "few": "{0} недели", + "many": "{0} недель", + "other": "{0} недели", + }, + "day": { + "one": "{0} день", + "few": "{0} дня", + "many": "{0} дней", + "other": "{0} дня", + }, + "hour": { + "one": "{0} час", + "few": "{0} часа", + "many": "{0} часов", + "other": "{0} часа", + }, + "minute": { + "one": "{0} минута", + "few": "{0} минуты", + "many": "{0} минут", + "other": "{0} минуты", + }, + "second": { + "one": "{0} секунда", + "few": "{0} секунды", + "many": "{0} секунд", + "other": "{0} секунды", + }, + "microsecond": { + "one": "{0} микросекунда", + "few": "{0} микросекунды", + "many": "{0} микросекунд", + "other": "{0} микросекунды", + }, + }, + "relative": { + "year": { + "future": { + "other": "через {0} года", + "one": "через {0} год", + "few": "через {0} года", + "many": "через {0} лет", + }, + "past": { + "other": "{0} года назад", + "one": "{0} год назад", + "few": "{0} года назад", + "many": "{0} лет назад", + }, + }, + "month": { + "future": { + "other": "через {0} месяца", + "one": "через {0} месяц", + "few": "через {0} месяца", + "many": "через {0} месяцев", + }, + "past": { + "other": "{0} месяца назад", + "one": "{0} месяц назад", + "few": "{0} месяца назад", + "many": "{0} месяцев назад", + }, + }, + "week": { + "future": { + "other": "через {0} недели", + "one": "через {0} неделю", + "few": "через {0} недели", + "many": "через {0} недель", + }, + "past": { + "other": "{0} недели назад", + "one": "{0} неделю назад", + "few": "{0} недели назад", + "many": "{0} недель назад", + }, + }, + "day": { + "future": { + "other": "через {0} дня", + "one": "через {0} день", + "few": "через {0} дня", + "many": "через {0} дней", + }, + "past": { + "other": "{0} дня назад", + "one": "{0} день назад", + "few": "{0} дня назад", + "many": "{0} дней назад", + }, + }, + "hour": { + "future": { + "other": "через {0} часа", + "one": "через {0} час", + "few": "через {0} часа", + "many": "через {0} часов", + }, + "past": { + "other": "{0} часа назад", + "one": "{0} час назад", + "few": "{0} часа назад", + "many": "{0} часов назад", + }, + }, + "minute": { + "future": { + "other": "через {0} минуты", + "one": "через {0} минуту", + "few": "через {0} минуты", + "many": "через {0} минут", + }, + "past": { + "other": "{0} минуты назад", + "one": "{0} минуту назад", + "few": "{0} минуты назад", + "many": "{0} минут назад", + }, + }, + "second": { + "future": { + "other": "через {0} секунды", + "one": "через {0} секунду", + "few": "через {0} секунды", + "many": "через {0} секунд", + }, + "past": { + "other": "{0} секунды назад", + "one": "{0} секунду назад", + "few": "{0} секунды назад", + "many": "{0} секунд назад", + }, + }, + }, + "day_periods": { + "midnight": "полночь", + "am": "AM", + "noon": "полдень", + "pm": "PM", + "morning1": "утра", + "afternoon1": "дня", + "evening1": "вечера", + "night1": "ночи", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/sk/__init__.py b/src/pendulum/locales/sk/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/sk/__init__.py diff --git a/src/pendulum/locales/sk/custom.py b/src/pendulum/locales/sk/custom.py new file mode 100644 index 0000000..0059c11 --- /dev/null +++ b/src/pendulum/locales/sk/custom.py @@ -0,0 +1,22 @@ +""" +sk custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "ago": "pred {}", + "from_now": "o {}", + "after": "{0} po", + "before": "{0} pred", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd, D. MMMM YYYY HH:mm", + "LLL": "D. MMMM YYYY HH:mm", + "LL": "D. MMMM YYYY", + "L": "DD.MM.YYYY", + }, +} diff --git a/src/pendulum/locales/sk/locale.py b/src/pendulum/locales/sk/locale.py new file mode 100644 index 0000000..530303f --- /dev/null +++ b/src/pendulum/locales/sk/locale.py @@ -0,0 +1,274 @@ +from __future__ import annotations + +from pendulum.locales.sk.custom import translations as custom_translations + + +""" +sk locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "few" + if ((n == n and (n >= 2 and n <= 4)) and (0 == 0 and (0 == 0))) + else "many" + if (not (0 == 0 and (0 == 0))) + else "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "po", + 1: "ut", + 2: "st", + 3: "št", + 4: "pi", + 5: "so", + 6: "ne", + }, + "narrow": { + 0: "p", + 1: "u", + 2: "s", + 3: "š", + 4: "p", + 5: "s", + 6: "n", + }, + "short": { + 0: "po", + 1: "ut", + 2: "st", + 3: "št", + 4: "pi", + 5: "so", + 6: "ne", + }, + "wide": { + 0: "pondelok", + 1: "utorok", + 2: "streda", + 3: "štvrtok", + 4: "piatok", + 5: "sobota", + 6: "nedeľa", + }, + }, + "months": { + "abbreviated": { + 1: "jan", + 2: "feb", + 3: "mar", + 4: "apr", + 5: "máj", + 6: "jún", + 7: "júl", + 8: "aug", + 9: "sep", + 10: "okt", + 11: "nov", + 12: "dec", + }, + "narrow": { + 1: "j", + 2: "f", + 3: "m", + 4: "a", + 5: "m", + 6: "j", + 7: "j", + 8: "a", + 9: "s", + 10: "o", + 11: "n", + 12: "d", + }, + "wide": { + 1: "januára", + 2: "februára", + 3: "marca", + 4: "apríla", + 5: "mája", + 6: "júna", + 7: "júla", + 8: "augusta", + 9: "septembra", + 10: "októbra", + 11: "novembra", + 12: "decembra", + }, + }, + "units": { + "year": { + "one": "{0} rok", + "few": "{0} roky", + "many": "{0} roka", + "other": "{0} rokov", + }, + "month": { + "one": "{0} mesiac", + "few": "{0} mesiace", + "many": "{0} mesiaca", + "other": "{0} mesiacov", + }, + "week": { + "one": "{0} týždeň", + "few": "{0} týždne", + "many": "{0} týždňa", + "other": "{0} týždňov", + }, + "day": { + "one": "{0} deň", + "few": "{0} dni", + "many": "{0} dňa", + "other": "{0} dní", + }, + "hour": { + "one": "{0} hodina", + "few": "{0} hodiny", + "many": "{0} hodiny", + "other": "{0} hodín", + }, + "minute": { + "one": "{0} minúta", + "few": "{0} minúty", + "many": "{0} minúty", + "other": "{0} minút", + }, + "second": { + "one": "{0} sekunda", + "few": "{0} sekundy", + "many": "{0} sekundy", + "other": "{0} sekúnd", + }, + "microsecond": { + "one": "{0} mikrosekunda", + "few": "{0} mikrosekundy", + "many": "{0} mikrosekundy", + "other": "{0} mikrosekúnd", + }, + }, + "relative": { + "year": { + "future": { + "other": "o {0} rokov", + "one": "o {0} rok", + "few": "o {0} roky", + "many": "o {0} roka", + }, + "past": { + "other": "pred {0} rokmi", + "one": "pred {0} rokom", + "few": "pred {0} rokmi", + "many": "pred {0} roka", + }, + }, + "month": { + "future": { + "other": "o {0} mesiacov", + "one": "o {0} mesiac", + "few": "o {0} mesiace", + "many": "o {0} mesiaca", + }, + "past": { + "other": "pred {0} mesiacmi", + "one": "pred {0} mesiacom", + "few": "pred {0} mesiacmi", + "many": "pred {0} mesiaca", + }, + }, + "week": { + "future": { + "other": "o {0} týždňov", + "one": "o {0} týždeň", + "few": "o {0} týždne", + "many": "o {0} týždňa", + }, + "past": { + "other": "pred {0} týždňami", + "one": "pred {0} týždňom", + "few": "pred {0} týždňami", + "many": "pred {0} týždňa", + }, + }, + "day": { + "future": { + "other": "o {0} dní", + "one": "o {0} deň", + "few": "o {0} dni", + "many": "o {0} dňa", + }, + "past": { + "other": "pred {0} dňami", + "one": "pred {0} dňom", + "few": "pred {0} dňami", + "many": "pred {0} dňa", + }, + }, + "hour": { + "future": { + "other": "o {0} hodín", + "one": "o {0} hodinu", + "few": "o {0} hodiny", + "many": "o {0} hodiny", + }, + "past": { + "other": "pred {0} hodinami", + "one": "pred {0} hodinou", + "few": "pred {0} hodinami", + "many": "pred {0} hodinou", + }, + }, + "minute": { + "future": { + "other": "o {0} minút", + "one": "o {0} minútu", + "few": "o {0} minúty", + "many": "o {0} minúty", + }, + "past": { + "other": "pred {0} minútami", + "one": "pred {0} minútou", + "few": "pred {0} minútami", + "many": "pred {0} minúty", + }, + }, + "second": { + "future": { + "other": "o {0} sekúnd", + "one": "o {0} sekundu", + "few": "o {0} sekundy", + "many": "o {0} sekundy", + }, + "past": { + "other": "pred {0} sekundami", + "one": "pred {0} sekundou", + "few": "pred {0} sekundami", + "many": "pred {0} sekundy", + }, + }, + }, + "day_periods": { + "midnight": "o polnoci", + "am": "AM", + "noon": "napoludnie", + "pm": "PM", + "morning1": "ráno", + "morning2": "dopoludnia", + "afternoon1": "popoludní", + "evening1": "večer", + "night1": "v noci", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/sv/__init__.py b/src/pendulum/locales/sv/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/sv/__init__.py diff --git a/src/pendulum/locales/sv/custom.py b/src/pendulum/locales/sv/custom.py new file mode 100644 index 0000000..83f36b1 --- /dev/null +++ b/src/pendulum/locales/sv/custom.py @@ -0,0 +1,22 @@ +""" +sv custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "ago": "{} sedan", + "from_now": "från nu {}", + "after": "{0} efter", + "before": "{0} innan", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "L": "YYYY-MM-DD", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY, HH:mm", + "LLLL": "dddd, D MMMM YYYY, HH:mm", + }, +} diff --git a/src/pendulum/locales/sv/locale.py b/src/pendulum/locales/sv/locale.py new file mode 100644 index 0000000..c3c4472 --- /dev/null +++ b/src/pendulum/locales/sv/locale.py @@ -0,0 +1,230 @@ +from __future__ import annotations + +from pendulum.locales.sv.custom import translations as custom_translations + + +""" +sv locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "one" + if ( + ((n % 10) == (n % 10) and (((n % 10) == 1) or ((n % 10) == 2))) + and (not ((n % 100) == (n % 100) and (((n % 100) == 11) or ((n % 100) == 12)))) + ) + else "other", + "translations": { + "days": { + "abbreviated": { + 0: "mån", + 1: "tis", + 2: "ons", + 3: "tors", + 4: "fre", + 5: "lör", + 6: "sön", + }, + "narrow": { + 0: "M", + 1: "T", + 2: "O", + 3: "T", + 4: "F", + 5: "L", + 6: "S", + }, + "short": { + 0: "må", + 1: "ti", + 2: "on", + 3: "to", + 4: "fr", + 5: "lö", + 6: "sö", + }, + "wide": { + 0: "måndag", + 1: "tisdag", + 2: "onsdag", + 3: "torsdag", + 4: "fredag", + 5: "lördag", + 6: "söndag", + }, + }, + "months": { + "abbreviated": { + 1: "jan.", + 2: "feb.", + 3: "mars", + 4: "apr.", + 5: "maj", + 6: "juni", + 7: "juli", + 8: "aug.", + 9: "sep.", + 10: "okt.", + 11: "nov.", + 12: "dec.", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "januari", + 2: "februari", + 3: "mars", + 4: "april", + 5: "maj", + 6: "juni", + 7: "juli", + 8: "augusti", + 9: "september", + 10: "oktober", + 11: "november", + 12: "december", + }, + }, + "units": { + "year": { + "one": "{0} år", + "other": "{0} år", + }, + "month": { + "one": "{0} månad", + "other": "{0} månader", + }, + "week": { + "one": "{0} vecka", + "other": "{0} veckor", + }, + "day": { + "one": "{0} dygn", + "other": "{0} dygn", + }, + "hour": { + "one": "{0} timme", + "other": "{0} timmar", + }, + "minute": { + "one": "{0} minut", + "other": "{0} minuter", + }, + "second": { + "one": "{0} sekund", + "other": "{0} sekunder", + }, + "microsecond": { + "one": "{0} mikrosekund", + "other": "{0} mikrosekunder", + }, + }, + "relative": { + "year": { + "future": { + "other": "om {0} år", + "one": "om {0} år", + }, + "past": { + "other": "för {0} år sedan", + "one": "för {0} år sedan", + }, + }, + "month": { + "future": { + "other": "om {0} månader", + "one": "om {0} månad", + }, + "past": { + "other": "för {0} månader sedan", + "one": "för {0} månad sedan", + }, + }, + "week": { + "future": { + "other": "om {0} veckor", + "one": "om {0} vecka", + }, + "past": { + "other": "för {0} veckor sedan", + "one": "för {0} vecka sedan", + }, + }, + "day": { + "future": { + "other": "om {0} dagar", + "one": "om {0} dag", + }, + "past": { + "other": "för {0} dagar sedan", + "one": "för {0} dag sedan", + }, + }, + "hour": { + "future": { + "other": "om {0} timmar", + "one": "om {0} timme", + }, + "past": { + "other": "för {0} timmar sedan", + "one": "för {0} timme sedan", + }, + }, + "minute": { + "future": { + "other": "om {0} minuter", + "one": "om {0} minut", + }, + "past": { + "other": "för {0} minuter sedan", + "one": "för {0} minut sedan", + }, + }, + "second": { + "future": { + "other": "om {0} sekunder", + "one": "om {0} sekund", + }, + "past": { + "other": "för {0} sekunder sedan", + "one": "för {0} sekund sedan", + }, + }, + }, + "day_periods": { + "midnight": "midnatt", + "am": "fm", + "pm": "em", + "morning1": "på morgonen", + "morning2": "på förmiddagen", + "afternoon1": "på eftermiddagen", + "evening1": "på kvällen", + "night1": "på natten", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/tr/__init__.py b/src/pendulum/locales/tr/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/tr/__init__.py diff --git a/src/pendulum/locales/tr/custom.py b/src/pendulum/locales/tr/custom.py new file mode 100644 index 0000000..b7fd3c7 --- /dev/null +++ b/src/pendulum/locales/tr/custom.py @@ -0,0 +1,24 @@ +""" +tr custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "ago": "{} önce", + "from_now": "{} içinde", + "after": "{0} sonra", + "before": "{0} önce", + # Ordinals + "ordinal": {"one": ".", "two": ".", "few": ".", "other": "."}, + # Date formats + "date_formats": { + "LTS": "h:mm:ss A", + "LT": "h:mm A", + "L": "MM/DD/YYYY", + "LL": "MMMM D, YYYY", + "LLL": "MMMM D, YYYY h:mm A", + "LLLL": "dddd, MMMM D, YYYY h:mm A", + }, +} diff --git a/src/pendulum/locales/tr/locale.py b/src/pendulum/locales/tr/locale.py new file mode 100644 index 0000000..f0233f2 --- /dev/null +++ b/src/pendulum/locales/tr/locale.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +from pendulum.locales.tr.custom import translations as custom_translations + + +""" +tr locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" if (n == n and (n == 1)) else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "Pzt", + 1: "Sal", + 2: "Çar", + 3: "Per", + 4: "Cum", + 5: "Cmt", + 6: "Paz", + }, + "narrow": { + 0: "P", + 1: "S", + 2: "Ç", + 3: "P", + 4: "C", + 5: "C", + 6: "P", + }, + "short": { + 0: "Pt", + 1: "Sa", + 2: "Ça", + 3: "Pe", + 4: "Cu", + 5: "Ct", + 6: "Pa", + }, + "wide": { + 0: "Pazartesi", + 1: "Salı", + 2: "Çarşamba", + 3: "Perşembe", + 4: "Cuma", + 5: "Cumartesi", + 6: "Pazar", + }, + }, + "months": { + "abbreviated": { + 1: "Oca", + 2: "Şub", + 3: "Mar", + 4: "Nis", + 5: "May", + 6: "Haz", + 7: "Tem", + 8: "Ağu", + 9: "Eyl", + 10: "Eki", + 11: "Kas", + 12: "Ara", + }, + "narrow": { + 1: "O", + 2: "Ş", + 3: "M", + 4: "N", + 5: "M", + 6: "H", + 7: "T", + 8: "A", + 9: "E", + 10: "E", + 11: "K", + 12: "A", + }, + "wide": { + 1: "Ocak", + 2: "Şubat", + 3: "Mart", + 4: "Nisan", + 5: "Mayıs", + 6: "Haziran", + 7: "Temmuz", + 8: "Ağustos", + 9: "Eylül", + 10: "Ekim", + 11: "Kasım", + 12: "Aralık", + }, + }, + "units": { + "year": { + "one": "{0} yıl", + "other": "{0} yıl", + }, + "month": { + "one": "{0} ay", + "other": "{0} ay", + }, + "week": { + "one": "{0} hafta", + "other": "{0} hafta", + }, + "day": { + "one": "{0} gün", + "other": "{0} gün", + }, + "hour": { + "one": "{0} saat", + "other": "{0} saat", + }, + "minute": { + "one": "{0} dakika", + "other": "{0} dakika", + }, + "second": { + "one": "{0} saniye", + "other": "{0} saniye", + }, + "microsecond": { + "one": "{0} mikrosaniye", + "other": "{0} mikrosaniye", + }, + }, + "relative": { + "year": { + "future": { + "other": "{0} yıl sonra", + "one": "{0} yıl sonra", + }, + "past": { + "other": "{0} yıl önce", + "one": "{0} yıl önce", + }, + }, + "month": { + "future": { + "other": "{0} ay sonra", + "one": "{0} ay sonra", + }, + "past": { + "other": "{0} ay önce", + "one": "{0} ay önce", + }, + }, + "week": { + "future": { + "other": "{0} hafta sonra", + "one": "{0} hafta sonra", + }, + "past": { + "other": "{0} hafta önce", + "one": "{0} hafta önce", + }, + }, + "day": { + "future": { + "other": "{0} gün sonra", + "one": "{0} gün sonra", + }, + "past": { + "other": "{0} gün önce", + "one": "{0} gün önce", + }, + }, + "hour": { + "future": { + "other": "{0} saat sonra", + "one": "{0} saat sonra", + }, + "past": { + "other": "{0} saat önce", + "one": "{0} saat önce", + }, + }, + "minute": { + "future": { + "other": "{0} dakika sonra", + "one": "{0} dakika sonra", + }, + "past": { + "other": "{0} dakika önce", + "one": "{0} dakika önce", + }, + }, + "second": { + "future": { + "other": "{0} saniye sonra", + "one": "{0} saniye sonra", + }, + "past": { + "other": "{0} saniye önce", + "one": "{0} saniye önce", + }, + }, + }, + "day_periods": { + "midnight": "gece yarısı", + "am": "ÖÖ", + "noon": "öğle", + "pm": "ÖS", + "morning1": "sabah", + "morning2": "öğleden önce", + "afternoon1": "öğleden sonra", + "afternoon2": "akşamüstü", + "evening1": "akşam", + "night1": "gece", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/zh/__init__.py b/src/pendulum/locales/zh/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/locales/zh/__init__.py diff --git a/src/pendulum/locales/zh/custom.py b/src/pendulum/locales/zh/custom.py new file mode 100644 index 0000000..cf47a40 --- /dev/null +++ b/src/pendulum/locales/zh/custom.py @@ -0,0 +1,20 @@ +""" +zh custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "after": "{time}后", + "before": "{time}前", + # Date formats + "date_formats": { + "LTS": "Ah点m分s秒", + "LT": "Ah点mm分", + "LLLL": "YYYY年MMMD日ddddAh点mm分", + "LLL": "YYYY年MMMD日Ah点mm分", + "LL": "YYYY年MMMD日", + "L": "YYYY-MM-DD", + }, +} diff --git a/src/pendulum/locales/zh/locale.py b/src/pendulum/locales/zh/locale.py new file mode 100644 index 0000000..eb85ab7 --- /dev/null +++ b/src/pendulum/locales/zh/locale.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +from pendulum.locales.zh.custom import translations as custom_translations + + +""" +zh locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "周一", + 1: "周二", + 2: "周三", + 3: "周四", + 4: "周五", + 5: "周六", + 6: "周日", + }, + "narrow": {0: "一", 1: "二", 2: "三", 3: "四", 4: "五", 5: "六", 6: "日"}, + "short": {0: "周一", 1: "周二", 2: "周三", 3: "周四", 4: "周五", 5: "周六", 6: "周日"}, + "wide": { + 0: "星期一", + 1: "星期二", + 2: "星期三", + 3: "星期四", + 4: "星期五", + 5: "星期六", + 6: "星期日", + }, + }, + "months": { + "abbreviated": { + 1: "1月", + 2: "2月", + 3: "3月", + 4: "4月", + 5: "5月", + 6: "6月", + 7: "7月", + 8: "8月", + 9: "9月", + 10: "10月", + 11: "11月", + 12: "12月", + }, + "narrow": { + 1: "1", + 2: "2", + 3: "3", + 4: "4", + 5: "5", + 6: "6", + 7: "7", + 8: "8", + 9: "9", + 10: "10", + 11: "11", + 12: "12", + }, + "wide": { + 1: "一月", + 2: "二月", + 3: "三月", + 4: "四月", + 5: "五月", + 6: "六月", + 7: "七月", + 8: "八月", + 9: "九月", + 10: "十月", + 11: "十一月", + 12: "十二月", + }, + }, + "units": { + "year": {"other": "{0}年"}, + "month": {"other": "{0}个月"}, + "week": {"other": "{0}周"}, + "day": {"other": "{0}天"}, + "hour": {"other": "{0}小时"}, + "minute": {"other": "{0}分钟"}, + "second": {"other": "{0}秒钟"}, + "microsecond": {"other": "{0}微秒"}, + }, + "relative": { + "year": {"future": {"other": "{0}年后"}, "past": {"other": "{0}年前"}}, + "month": {"future": {"other": "{0}个月后"}, "past": {"other": "{0}个月前"}}, + "week": {"future": {"other": "{0}周后"}, "past": {"other": "{0}周前"}}, + "day": {"future": {"other": "{0}天后"}, "past": {"other": "{0}天前"}}, + "hour": {"future": {"other": "{0}小时后"}, "past": {"other": "{0}小时前"}}, + "minute": {"future": {"other": "{0}分钟后"}, "past": {"other": "{0}分钟前"}}, + "second": {"future": {"other": "{0}秒钟后"}, "past": {"other": "{0}秒钟前"}}, + }, + "day_periods": { + "midnight": "午夜", + "am": "上午", + "pm": "下午", + "morning1": "清晨", + "morning2": "上午", + "afternoon1": "下午", + "afternoon2": "下午", + "evening1": "晚上", + "night1": "凌晨", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/mixins/__init__.py b/src/pendulum/mixins/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/mixins/__init__.py diff --git a/src/pendulum/mixins/default.py b/src/pendulum/mixins/default.py new file mode 100644 index 0000000..f2531f3 --- /dev/null +++ b/src/pendulum/mixins/default.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from pendulum.formatting import Formatter + + +_formatter = Formatter() + + +class FormattableMixin: + _formatter: Formatter = _formatter + + def format(self, fmt: str, locale: str | None = None) -> str: + """ + Formats the instance using the given format. + + :param fmt: The format to use + :param locale: The locale to use + """ + return self._formatter.format(self, fmt, locale) + + def for_json(self) -> str: + """ + Methods for automatic json serialization by simplejson. + """ + return self.isoformat() + + def __format__(self, format_spec: str) -> str: + if len(format_spec) > 0: + if "%" in format_spec: + return self.strftime(format_spec) + + return self.format(format_spec) + + return str(self) + + def __str__(self) -> str: + return self.isoformat() diff --git a/src/pendulum/parser.py b/src/pendulum/parser.py new file mode 100644 index 0000000..5f9a0f7 --- /dev/null +++ b/src/pendulum/parser.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +import datetime +import typing as t + +import pendulum + +from pendulum.duration import Duration +from pendulum.parsing import _Interval +from pendulum.parsing import parse as base_parse +from pendulum.tz.timezone import UTC + + +if t.TYPE_CHECKING: + from pendulum.date import Date + from pendulum.datetime import DateTime + from pendulum.interval import Interval + from pendulum.time import Time + +try: + from pendulum._pendulum import Duration as RustDuration +except ImportError: + RustDuration = None # type: ignore[assignment,misc] + + +def parse(text: str, **options: t.Any) -> Date | Time | DateTime | Duration: + # Use the mock now value if it exists + options["now"] = options.get("now") + + return _parse(text, **options) + + +def _parse(text: str, **options: t.Any) -> Date | DateTime | Time | Duration | Interval: + """ + Parses a string with the given options. + + :param text: The string to parse. + """ + # Handling special cases + if text == "now": + return pendulum.now() + + parsed = base_parse(text, **options) + + if isinstance(parsed, datetime.datetime): + return pendulum.datetime( + parsed.year, + parsed.month, + parsed.day, + parsed.hour, + parsed.minute, + parsed.second, + parsed.microsecond, + tz=parsed.tzinfo or options.get("tz", UTC), + ) + + if isinstance(parsed, datetime.date): + return pendulum.date(parsed.year, parsed.month, parsed.day) + + if isinstance(parsed, datetime.time): + return pendulum.time( + parsed.hour, parsed.minute, parsed.second, parsed.microsecond + ) + + if isinstance(parsed, _Interval): + if parsed.duration is not None: + duration = parsed.duration + + if parsed.start is not None: + dt = pendulum.instance(parsed.start, tz=options.get("tz", UTC)) + + return pendulum.interval( + dt, + dt.add( + years=duration.years, + months=duration.months, + weeks=duration.weeks, + days=duration.remaining_days, + hours=duration.hours, + minutes=duration.minutes, + seconds=duration.remaining_seconds, + microseconds=duration.microseconds, + ), + ) + + dt = pendulum.instance( + t.cast(datetime.datetime, parsed.end), tz=options.get("tz", UTC) + ) + + return pendulum.interval( + dt.subtract( + years=duration.years, + months=duration.months, + weeks=duration.weeks, + days=duration.remaining_days, + hours=duration.hours, + minutes=duration.minutes, + seconds=duration.remaining_seconds, + microseconds=duration.microseconds, + ), + dt, + ) + + return pendulum.interval( + pendulum.instance( + t.cast(datetime.datetime, parsed.start), tz=options.get("tz", UTC) + ), + pendulum.instance( + t.cast(datetime.datetime, parsed.end), tz=options.get("tz", UTC) + ), + ) + + if isinstance(parsed, Duration): + return parsed + + if RustDuration is not None and isinstance(parsed, RustDuration): + return pendulum.duration( + years=parsed.years, + months=parsed.months, + weeks=parsed.weeks, + days=parsed.days, + hours=parsed.hours, + minutes=parsed.minutes, + seconds=parsed.seconds, + microseconds=parsed.microseconds, + ) + + raise NotImplementedError diff --git a/src/pendulum/parsing/__init__.py b/src/pendulum/parsing/__init__.py new file mode 100644 index 0000000..761f52c --- /dev/null +++ b/src/pendulum/parsing/__init__.py @@ -0,0 +1,235 @@ +from __future__ import annotations + +import contextlib +import copy +import os +import re +import struct + +from datetime import date +from datetime import datetime +from datetime import time +from typing import Any +from typing import Optional +from typing import cast + +from dateutil import parser + +from pendulum.parsing.exceptions import ParserError + + +with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1" + +try: + if not with_extensions or struct.calcsize("P") == 4: + raise ImportError() + + from pendulum._pendulum import Duration + from pendulum._pendulum import parse_iso8601 +except ImportError: + from pendulum.duration import Duration # type: ignore[assignment] + from pendulum.parsing.iso8601 import parse_iso8601 # type: ignore[assignment] + + +COMMON = re.compile( + # Date (optional) # noqa: ERA001 + "^" + "(?P<date>" + " (?P<classic>" # Classic date (YYYY-MM-DD) + r" (?P<year>\d{4})" # Year + " (?P<monthday>" + r" (?P<monthsep>[/:])?(?P<month>\d{2})" # Month (optional) + r" ((?P<daysep>[/:])?(?P<day>\d{2}))" # Day (optional) + " )?" + " )" + ")?" + # Time (optional) # noqa: ERA001 + "(?P<time>" r" (?P<timesep>\ )?" # Separator (space) + # HH:mm:ss (optional mm and ss) + r" (?P<hour>\d{1,2}):(?P<minute>\d{1,2})?(?::(?P<second>\d{1,2}))?" + # Subsecond part (optional) + " (?P<subsecondsection>" + " (?:[.|,])" # Subsecond separator (optional) + r" (?P<subsecond>\d{1,9})" # Subsecond + " )?" + ")?" + "$", + re.VERBOSE, +) + +DEFAULT_OPTIONS = { + "day_first": False, + "year_first": True, + "strict": True, + "exact": False, + "now": None, +} + + +def parse(text: str, **options: Any) -> datetime | date | time | _Interval | Duration: + """ + Parses a string with the given options. + + :param text: The string to parse. + """ + _options: dict[str, Any] = copy.copy(DEFAULT_OPTIONS) + _options.update(options) + + return _normalize(_parse(text, **_options), **_options) + + +def _normalize( + parsed: datetime | date | time | _Interval | Duration, **options: Any +) -> datetime | date | time | _Interval | Duration: + """ + Normalizes the parsed element. + + :param parsed: The parsed elements. + """ + if options.get("exact"): + return parsed + + if isinstance(parsed, time): + now = cast(Optional[datetime], options["now"]) or datetime.now() + + return datetime( + now.year, + now.month, + now.day, + parsed.hour, + parsed.minute, + parsed.second, + parsed.microsecond, + ) + elif isinstance(parsed, date) and not isinstance(parsed, datetime): + return datetime(parsed.year, parsed.month, parsed.day) + + return parsed + + +def _parse(text: str, **options: Any) -> datetime | date | time | _Interval | Duration: + # Trying to parse ISO8601 + with contextlib.suppress(ValueError): + return parse_iso8601(text) + + with contextlib.suppress(ValueError): + return _parse_iso8601_interval(text) + + with contextlib.suppress(ParserError): + return _parse_common(text, **options) + + # We couldn't parse the string + # so we fallback on the dateutil parser + # If not strict + if options.get("strict", True): + raise ParserError(f"Unable to parse string [{text}]") + + try: + dt = parser.parse( + text, dayfirst=options["day_first"], yearfirst=options["year_first"] + ) + except ValueError: + raise ParserError(f"Invalid date string: {text}") + + return dt + + +def _parse_common(text: str, **options: Any) -> datetime | date | time: + """ + Tries to parse the string as a common datetime format. + + :param text: The string to parse. + """ + m = COMMON.match(text) + has_date = False + year = 0 + month = 1 + day = 1 + + if not m: + raise ParserError("Invalid datetime string") + + if m.group("date"): + # A date has been specified + has_date = True + + year = int(m.group("year")) + + if not m.group("monthday"): + # No month and day + month = 1 + day = 1 + else: + if options["day_first"]: + month = int(m.group("day")) + day = int(m.group("month")) + else: + month = int(m.group("month")) + day = int(m.group("day")) + + if not m.group("time"): + return date(year, month, day) + + # Grabbing hh:mm:ss + hour = int(m.group("hour")) + + minute = int(m.group("minute")) + + second = int(m.group("second")) if m.group("second") else 0 + + # Grabbing subseconds, if any + microsecond = 0 + if m.group("subsecondsection"): + # Limiting to 6 chars + subsecond = m.group("subsecond")[:6] + + microsecond = int(f"{subsecond:0<6}") + + if has_date: + return datetime(year, month, day, hour, minute, second, microsecond) + + return time(hour, minute, second, microsecond) + + +class _Interval: + """ + Special class to handle ISO 8601 intervals + """ + + def __init__( + self, + start: datetime | None = None, + end: datetime | None = None, + duration: Duration | None = None, + ) -> None: + self.start = start + self.end = end + self.duration = duration + + +def _parse_iso8601_interval(text: str) -> _Interval: + if "/" not in text: + raise ParserError("Invalid interval") + + first, last = text.split("/") + start = end = duration = None + + if first[0] == "P": + # duration/end + duration = parse_iso8601(first) + end = parse_iso8601(last) + elif last[0] == "P": + # start/duration + start = parse_iso8601(first) + duration = parse_iso8601(last) + else: + # start/end + start = parse_iso8601(first) + end = parse_iso8601(last) + + return _Interval( + cast(datetime, start), cast(datetime, end), cast(Duration, duration) + ) + + +__all__ = ["parse", "parse_iso8601"] diff --git a/src/pendulum/parsing/exceptions/__init__.py b/src/pendulum/parsing/exceptions/__init__.py new file mode 100644 index 0000000..9f2d809 --- /dev/null +++ b/src/pendulum/parsing/exceptions/__init__.py @@ -0,0 +1,5 @@ +from __future__ import annotations + + +class ParserError(ValueError): + pass diff --git a/src/pendulum/parsing/iso8601.py b/src/pendulum/parsing/iso8601.py new file mode 100644 index 0000000..cc4dd7a --- /dev/null +++ b/src/pendulum/parsing/iso8601.py @@ -0,0 +1,453 @@ +from __future__ import annotations + +import datetime +import re + +from typing import cast + +from pendulum.constants import HOURS_PER_DAY +from pendulum.constants import MINUTES_PER_HOUR +from pendulum.constants import MONTHS_OFFSETS +from pendulum.constants import SECONDS_PER_MINUTE +from pendulum.duration import Duration +from pendulum.helpers import days_in_year +from pendulum.helpers import is_leap +from pendulum.helpers import is_long_year +from pendulum.helpers import week_day +from pendulum.parsing.exceptions import ParserError +from pendulum.tz.timezone import UTC +from pendulum.tz.timezone import FixedTimezone +from pendulum.tz.timezone import Timezone + + +ISO8601_DT = re.compile( + # Date (optional) # noqa: ERA001 + "^" + "(?P<date>" + " (?P<classic>" # Classic date (YYYY-MM-DD) or ordinal (YYYY-DDD) + r" (?P<year>\d{4})" # Year + " (?P<monthday>" + r" (?P<monthsep>-)?(?P<month>\d{2})" # Month (optional) + r" ((?P<daysep>-)?(?P<day>\d{1,2}))?" # Day (optional) + " )?" + " )" + " |" + " (?P<isocalendar>" # Calendar date (2016-W05 or 2016-W05-5) + r" (?P<isoyear>\d{4})" # Year + " (?P<weeksep>-)?" # Separator (optional) + " W" # W separator + r" (?P<isoweek>\d{2})" # Week number + " (?P<weekdaysep>-)?" # Separator (optional) + r" (?P<isoweekday>\d)?" # Weekday (optional) + " )" + ")?" + # Time (optional) # noqa: ERA001 + "(?P<time>" r" (?P<timesep>[T\ ])?" # Separator (T or space) + # HH:mm:ss (optional mm and ss) + r" (?P<hour>\d{1,2})(?P<minsep>:)?(?P<minute>\d{1,2})?(?P<secsep>:)?(?P<second>\d{1,2})?" # noqa: E501 + # Subsecond part (optional) + " (?P<subsecondsection>" + " (?:[.,])" # Subsecond separator (optional) + r" (?P<subsecond>\d{1,9})" # Subsecond + " )?" + # Timezone offset + " (?P<tz>" + r" (?:[-+])\d{2}:?(?:\d{2})?|Z" # Offset (+HH:mm or +HHmm or +HH or Z) + " )?" + ")?" + "$", + re.VERBOSE, +) + +ISO8601_DURATION = re.compile( + "^P" # Duration P indicator + # Years, months and days (optional) # noqa: ERA001 + "(?P<w>" + r" (?P<weeks>\d+(?:[.,]\d+)?W)" + ")?" + "(?P<ymd>" + r" (?P<years>\d+(?:[.,]\d+)?Y)?" + r" (?P<months>\d+(?:[.,]\d+)?M)?" + r" (?P<days>\d+(?:[.,]\d+)?D)?" + ")?" + "(?P<hms>" + " (?P<timesep>T)" # Separator (T) + r" (?P<hours>\d+(?:[.,]\d+)?H)?" + r" (?P<minutes>\d+(?:[.,]\d+)?M)?" + r" (?P<seconds>\d+(?:[.,]\d+)?S)?" + ")?" + "$", + re.VERBOSE, +) + + +def parse_iso8601( + text: str, +) -> datetime.datetime | datetime.date | datetime.time | Duration: + """ + ISO 8601 compliant parser. + + :param text: The string to parse + :type text: str + + :rtype: datetime.datetime or datetime.time or datetime.date + """ + parsed = _parse_iso8601_duration(text) + if parsed is not None: + return parsed + + m = ISO8601_DT.match(text) + if not m: + raise ParserError("Invalid ISO 8601 string") + + ambiguous_date = False + is_date = False + is_time = False + year = 0 + month = 1 + day = 1 + minute = 0 + second = 0 + microsecond = 0 + tzinfo: FixedTimezone | Timezone | None = None + + if m.group("date"): + # A date has been specified + is_date = True + + if m.group("isocalendar"): + # We have a ISO 8601 string defined + # by week number + if ( + m.group("weeksep") + and not m.group("weekdaysep") + and m.group("isoweekday") + ): + raise ParserError(f"Invalid date string: {text}") + + if not m.group("weeksep") and m.group("weekdaysep"): + raise ParserError(f"Invalid date string: {text}") + + try: + date = _get_iso_8601_week( + m.group("isoyear"), m.group("isoweek"), m.group("isoweekday") + ) + except ParserError: + raise + except ValueError: + raise ParserError(f"Invalid date string: {text}") + + year = date["year"] + month = date["month"] + day = date["day"] + else: + # We have a classic date representation + year = int(m.group("year")) + + if not m.group("monthday"): + # No month and day + month = 1 + day = 1 + else: + if m.group("month") and m.group("day"): + # Month and day + if not m.group("daysep") and len(m.group("day")) == 1: + # Ordinal day + ordinal = int(m.group("month") + m.group("day")) + leap = is_leap(year) + months_offsets = MONTHS_OFFSETS[leap] + + if ordinal > months_offsets[13]: + raise ParserError("Ordinal day is out of range") + + for i in range(1, 14): + if ordinal <= months_offsets[i]: + day = ordinal - months_offsets[i - 1] + month = i - 1 + + break + else: + month = int(m.group("month")) + day = int(m.group("day")) + else: + # Only month + if not m.group("monthsep"): + # The date looks like 201207 + # which is invalid for a date + # But it might be a time in the form hhmmss + ambiguous_date = True + + month = int(m.group("month")) + day = 1 + + if not m.group("time"): + # No time has been specified + if ambiguous_date: + # We can "safely" assume that the ambiguous date + # was actually a time in the form hhmmss + hhmmss = f"{year!s}{month!s:0>2}" + + return datetime.time(int(hhmmss[:2]), int(hhmmss[2:4]), int(hhmmss[4:])) + + return datetime.date(year, month, day) + + if ambiguous_date: + raise ParserError(f"Invalid date string: {text}") + + if is_date and not m.group("timesep"): + raise ParserError(f"Invalid date string: {text}") + + if not is_date: + is_time = True + + # Grabbing hh:mm:ss + hour = int(m.group("hour")) + minsep = m.group("minsep") + + if m.group("minute"): + minute = int(m.group("minute")) + elif minsep: + raise ParserError("Invalid ISO 8601 time part") + + secsep = m.group("secsep") + if secsep and not minsep and m.group("minute"): + # minute/second separator but no hour/minute separator + raise ParserError("Invalid ISO 8601 time part") + + if m.group("second"): + if not secsep and minsep: + # No minute/second separator but hour/minute separator + raise ParserError("Invalid ISO 8601 time part") + + second = int(m.group("second")) + elif secsep: + raise ParserError("Invalid ISO 8601 time part") + + # Grabbing subseconds, if any + if m.group("subsecondsection"): + # Limiting to 6 chars + subsecond = m.group("subsecond")[:6] + + microsecond = int(f"{subsecond:0<6}") + + # Grabbing timezone, if any + tz = m.group("tz") + if tz: + if tz == "Z": + tzinfo = UTC + else: + negative = bool(tz.startswith("-")) + tz = tz[1:] + if ":" not in tz: + if len(tz) == 2: + tz = f"{tz}00" + + off_hour = tz[0:2] + off_minute = tz[2:4] + else: + off_hour, off_minute = tz.split(":") + + offset = ((int(off_hour) * 60) + int(off_minute)) * 60 + + if negative: + offset = -1 * offset + + tzinfo = FixedTimezone(offset) + + if is_time: + return datetime.time(hour, minute, second, microsecond, tzinfo=tzinfo) + + return datetime.datetime( + year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo + ) + + +def _parse_iso8601_duration(text: str, **options: str) -> Duration | None: + m = ISO8601_DURATION.match(text) + if not m: + return None + + years = 0 + months = 0 + weeks = 0 + days: int | float = 0 + hours: int | float = 0 + minutes: int | float = 0 + seconds: int | float = 0 + microseconds: int | float = 0 + fractional = False + + _days: str | float + _hour: str | int | None + _minutes: str | int | None + _seconds: str | int | None + if m.group("w"): + # Weeks + if m.group("ymd") or m.group("hms"): + # Specifying anything more than weeks is not supported + raise ParserError("Invalid duration string") + + _weeks = m.group("weeks") + if not _weeks: + raise ParserError("Invalid duration string") + + _weeks = _weeks.replace(",", ".").replace("W", "") + if "." in _weeks: + _weeks, portion = _weeks.split(".") + weeks = int(_weeks) + _days = int(portion) / 10 * 7 + days, hours = int(_days // 1), int(_days % 1 * HOURS_PER_DAY) + else: + weeks = int(_weeks) + + if m.group("ymd"): + # Years, months and/or days + _years = m.group("years") + _months = m.group("months") + _days = m.group("days") + + # Checking order + years_start = m.start("years") if _years else -3 + months_start = m.start("months") if _months else years_start + 1 + days_start = m.start("days") if _days else months_start + 1 + + # Check correct order + if not (years_start < months_start < days_start): + raise ParserError("Invalid duration") + + if _years: + _years = _years.replace(",", ".").replace("Y", "") + if "." in _years: + raise ParserError("Float years in duration are not supported") + else: + years = int(_years) + + if _months: + if fractional: + raise ParserError("Invalid duration") + + _months = _months.replace(",", ".").replace("M", "") + if "." in _months: + raise ParserError("Float months in duration are not supported") + else: + months = int(_months) + + if _days: + if fractional: + raise ParserError("Invalid duration") + + _days = _days.replace(",", ".").replace("D", "") + + if "." in _days: + fractional = True + + _days, _hours = _days.split(".") + days = int(_days) + hours = int(_hours) / 10 * HOURS_PER_DAY + else: + days = int(_days) + + if m.group("hms"): + # Hours, minutes and/or seconds + _hours = m.group("hours") or 0 + _minutes = m.group("minutes") or 0 + _seconds = m.group("seconds") or 0 + + # Checking order + hours_start = m.start("hours") if _hours else -3 + minutes_start = m.start("minutes") if _minutes else hours_start + 1 + seconds_start = m.start("seconds") if _seconds else minutes_start + 1 + + # Check correct order + if not (hours_start < minutes_start < seconds_start): + raise ParserError("Invalid duration") + + if _hours: + if fractional: + raise ParserError("Invalid duration") + + _hours = cast(str, _hours).replace(",", ".").replace("H", "") + + if "." in _hours: + fractional = True + + _hours, _mins = _hours.split(".") + hours += int(_hours) + minutes += int(_mins) / 10 * MINUTES_PER_HOUR + else: + hours += int(_hours) + + if _minutes: + if fractional: + raise ParserError("Invalid duration") + + _minutes = cast(str, _minutes).replace(",", ".").replace("M", "") + + if "." in _minutes: + fractional = True + + _minutes, _secs = _minutes.split(".") + minutes += int(_minutes) + seconds += int(_secs) / 10 * SECONDS_PER_MINUTE + else: + minutes += int(_minutes) + + if _seconds: + if fractional: + raise ParserError("Invalid duration") + + _seconds = cast(str, _seconds).replace(",", ".").replace("S", "") + + if "." in _seconds: + _seconds, _microseconds = _seconds.split(".") + seconds += int(_seconds) + microseconds += int(f"{_microseconds[:6]:0<6}") + else: + seconds += int(_seconds) + + return Duration( + years=years, + months=months, + weeks=weeks, + days=days, + hours=hours, + minutes=minutes, + seconds=seconds, + microseconds=microseconds, + ) + + +def _get_iso_8601_week( + year: int | str, week: int | str, weekday: int | str +) -> dict[str, int]: + weekday = 1 if not weekday else int(weekday) + + year = int(year) + week = int(week) + + if week > 53 or week > 52 and not is_long_year(year): + raise ParserError("Invalid week for week date") + + if weekday > 7: + raise ParserError("Invalid weekday for week date") + + # We can't rely on strptime directly here since + # it does not support ISO week date + ordinal = week * 7 + weekday - (week_day(year, 1, 4) + 3) + + if ordinal < 1: + # Previous year + ordinal += days_in_year(year - 1) + year -= 1 + + if ordinal > days_in_year(year): + # Next year + ordinal -= days_in_year(year) + year += 1 + + fmt = "%Y-%j" + string = f"{year}-{ordinal}" + + dt = datetime.datetime.strptime(string, fmt) + + return {"year": dt.year, "month": dt.month, "day": dt.day} diff --git a/src/pendulum/py.typed b/src/pendulum/py.typed new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/py.typed diff --git a/src/pendulum/testing/__init__.py b/src/pendulum/testing/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/testing/__init__.py diff --git a/src/pendulum/testing/traveller.py b/src/pendulum/testing/traveller.py new file mode 100644 index 0000000..3ef3af4 --- /dev/null +++ b/src/pendulum/testing/traveller.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import cast + +from pendulum.datetime import DateTime +from pendulum.utils._compat import PYPY + + +if TYPE_CHECKING: + from types import TracebackType + + from typing_extensions import Self + + +class BaseTraveller: + def __init__(self, datetime_class: type[DateTime] = DateTime) -> None: + self._datetime_class: type[DateTime] = datetime_class + + def freeze(self) -> Self: + raise self._not_implemented() + + def travel_back(self) -> Self: + raise self._not_implemented() + + def travel( + self, + years: int = 0, + months: int = 0, + weeks: int = 0, + days: int = 0, + hours: int = 0, + minutes: int = 0, + seconds: int = 0, + microseconds: int = 0, + ) -> Self: + raise self._not_implemented() + + def travel_to(self, dt: DateTime, *, freeze: bool = False) -> Self: + raise self._not_implemented() + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType, + ) -> None: + ... + + def _not_implemented(self) -> NotImplementedError: + return NotImplementedError() + + +if not PYPY: + try: + import time_machine + except ImportError: + time_machine = None # type: ignore[assignment] + + if time_machine is not None: + + class Traveller(BaseTraveller): + def __init__(self, datetime_class: type[DateTime] = DateTime) -> None: + super().__init__(datetime_class) + + self._started: bool = False + self._traveller: time_machine.travel | None = None + self._coordinates: time_machine.Coordinates | None = None + + def freeze(self) -> Self: + if self._started: + cast(time_machine.Coordinates, self._coordinates).move_to( + self._datetime_class.now(), tick=False + ) + else: + self._start(freeze=True) + + return self + + def travel_back(self) -> Self: + if not self._started: + return self + + cast(time_machine.travel, self._traveller).stop() + self._coordinates = None + self._traveller = None + self._started = False + + return self + + def travel( + self, + years: int = 0, + months: int = 0, + weeks: int = 0, + days: int = 0, + hours: int = 0, + minutes: int = 0, + seconds: int = 0, + microseconds: int = 0, + *, + freeze: bool = False, + ) -> Self: + self._start(freeze=freeze) + + cast(time_machine.Coordinates, self._coordinates).move_to( + self._datetime_class.now().add( + years=years, + months=months, + weeks=weeks, + days=days, + hours=hours, + minutes=minutes, + seconds=seconds, + microseconds=microseconds, + ) + ) + + return self + + def travel_to(self, dt: DateTime, *, freeze: bool = False) -> Self: + self._start(freeze=freeze) + + cast(time_machine.Coordinates, self._coordinates).move_to(dt) + + return self + + def _start(self, freeze: bool = False) -> None: + if self._started: + return + + if not self._traveller: + self._traveller = time_machine.travel( + self._datetime_class.now(), tick=not freeze + ) + + self._coordinates = self._traveller.start() + + self._started = True + + def __enter__(self) -> Self: + self._start() + + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType, + ) -> None: + self.travel_back() + + else: + + class Traveller(BaseTraveller): # type: ignore[no-redef] + def _not_implemented(self) -> NotImplementedError: + return NotImplementedError( + "Time travelling is an optional feature. " + 'You can add it by installing Pendulum with the "test" extra.' + ) + +else: + + class Traveller(BaseTraveller): # type: ignore[no-redef] + def _not_implemented(self) -> NotImplementedError: + return NotImplementedError( + "Time travelling is not supported on the PyPy Python implementation." + ) diff --git a/src/pendulum/time.py b/src/pendulum/time.py new file mode 100644 index 0000000..23c79c0 --- /dev/null +++ b/src/pendulum/time.py @@ -0,0 +1,321 @@ +from __future__ import annotations + +import datetime + +from datetime import time +from datetime import timedelta +from typing import TYPE_CHECKING +from typing import Optional +from typing import cast +from typing import overload + +import pendulum + +from pendulum.constants import SECS_PER_HOUR +from pendulum.constants import SECS_PER_MIN +from pendulum.constants import USECS_PER_SEC +from pendulum.duration import AbsoluteDuration +from pendulum.duration import Duration +from pendulum.mixins.default import FormattableMixin +from pendulum.tz.timezone import UTC + + +if TYPE_CHECKING: + from typing_extensions import Literal + from typing_extensions import Self + from typing_extensions import SupportsIndex + + from pendulum.tz.timezone import FixedTimezone + from pendulum.tz.timezone import Timezone + + +class Time(FormattableMixin, time): + """ + Represents a time instance as hour, minute, second, microsecond. + """ + + @classmethod + def instance( + cls, t: time, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = UTC + ) -> Self: + tz = t.tzinfo or tz + + if tz is not None: + tz = pendulum._safe_timezone(tz) + + return cls(t.hour, t.minute, t.second, t.microsecond, tzinfo=tz, fold=t.fold) + + # String formatting + def __repr__(self) -> str: + us = "" + if self.microsecond: + us = f", {self.microsecond}" + + tzinfo = "" + if self.tzinfo: + tzinfo = f", tzinfo={self.tzinfo!r}" + + return ( + f"{self.__class__.__name__}" + f"({self.hour}, {self.minute}, {self.second}{us}{tzinfo})" + ) + + # Comparisons + + def closest(self, dt1: Time | time, dt2: Time | time) -> Self: + """ + Get the closest time from the instance. + """ + dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond) + dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond) + + if self.diff(dt1).in_seconds() < self.diff(dt2).in_seconds(): + return dt1 + + return dt2 + + def farthest(self, dt1: Time | time, dt2: Time | time) -> Self: + """ + Get the farthest time from the instance. + """ + dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond) + dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond) + + if self.diff(dt1).in_seconds() > self.diff(dt2).in_seconds(): + return dt1 + + return dt2 + + # ADDITIONS AND SUBSTRACTIONS + + def add( + self, hours: int = 0, minutes: int = 0, seconds: int = 0, microseconds: int = 0 + ) -> Time: + """ + Add duration to the instance. + + :param hours: The number of hours + :param minutes: The number of minutes + :param seconds: The number of seconds + :param microseconds: The number of microseconds + """ + from pendulum.datetime import DateTime + + return ( + DateTime.EPOCH.at(self.hour, self.minute, self.second, self.microsecond) + .add( + hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds + ) + .time() + ) + + def subtract( + self, hours: int = 0, minutes: int = 0, seconds: int = 0, microseconds: int = 0 + ) -> Time: + """ + Add duration to the instance. + + :param hours: The number of hours + :type hours: int + + :param minutes: The number of minutes + :type minutes: int + + :param seconds: The number of seconds + :type seconds: int + + :param microseconds: The number of microseconds + :type microseconds: int + + :rtype: Time + """ + from pendulum.datetime import DateTime + + return ( + DateTime.EPOCH.at(self.hour, self.minute, self.second, self.microsecond) + .subtract( + hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds + ) + .time() + ) + + def add_timedelta(self, delta: datetime.timedelta) -> Time: + """ + Add timedelta duration to the instance. + + :param delta: The timedelta instance + """ + if delta.days: + raise TypeError("Cannot add timedelta with days to Time.") + + return self.add(seconds=delta.seconds, microseconds=delta.microseconds) + + def subtract_timedelta(self, delta: datetime.timedelta) -> Time: + """ + Remove timedelta duration from the instance. + + :param delta: The timedelta instance + """ + if delta.days: + raise TypeError("Cannot subtract timedelta with days to Time.") + + return self.subtract(seconds=delta.seconds, microseconds=delta.microseconds) + + def __add__(self, other: datetime.timedelta) -> Time: + if not isinstance(other, timedelta): + return NotImplemented + + return self.add_timedelta(other) + + @overload + def __sub__(self, other: time) -> pendulum.Duration: + ... + + @overload + def __sub__(self, other: datetime.timedelta) -> Time: + ... + + def __sub__(self, other: time | datetime.timedelta) -> pendulum.Duration | Time: + if not isinstance(other, (Time, time, timedelta)): + return NotImplemented + + if isinstance(other, timedelta): + return self.subtract_timedelta(other) + + if isinstance(other, time): + if other.tzinfo is not None: + raise TypeError("Cannot subtract aware times to or from Time.") + + other = self.__class__( + other.hour, other.minute, other.second, other.microsecond + ) + + return other.diff(self, False) + + @overload + def __rsub__(self, other: time) -> pendulum.Duration: + ... + + @overload + def __rsub__(self, other: datetime.timedelta) -> Time: + ... + + def __rsub__(self, other: time | datetime.timedelta) -> pendulum.Duration | Time: + if not isinstance(other, (Time, time)): + return NotImplemented + + if isinstance(other, time): + if other.tzinfo is not None: + raise TypeError("Cannot subtract aware times to or from Time.") + + other = self.__class__( + other.hour, other.minute, other.second, other.microsecond + ) + + return other.__sub__(self) + + # DIFFERENCES + + def diff(self, dt: time | None = None, abs: bool = True) -> Duration: + """ + Returns the difference between two Time objects as an Duration. + + :param dt: The time to subtract from + :param abs: Whether to return an absolute duration or not + """ + if dt is None: + dt = pendulum.now().time() + else: + dt = self.__class__(dt.hour, dt.minute, dt.second, dt.microsecond) + + us1 = ( + self.hour * SECS_PER_HOUR + self.minute * SECS_PER_MIN + self.second + ) * USECS_PER_SEC + + us2 = ( + dt.hour * SECS_PER_HOUR + dt.minute * SECS_PER_MIN + dt.second + ) * USECS_PER_SEC + + klass = Duration + if abs: + klass = AbsoluteDuration + + return klass(microseconds=us2 - us1) + + def diff_for_humans( + self, + other: time | None = None, + absolute: bool = False, + locale: str | None = None, + ) -> str: + """ + Get the difference in a human readable format in the current locale. + + :param dt: The time to subtract from + :param absolute: removes time difference modifiers ago, after, etc + :param locale: The locale to use for localization + """ + is_now = other is None + + if is_now: + other = pendulum.now().time() + + diff = self.diff(other) + + return pendulum.format_diff(diff, is_now, absolute, locale) + + # Compatibility methods + + def replace( + self, + hour: SupportsIndex | None = None, + minute: SupportsIndex | None = None, + second: SupportsIndex | None = None, + microsecond: SupportsIndex | None = None, + tzinfo: bool | datetime.tzinfo | Literal[True] | None = True, + fold: int = 0, + ) -> Self: + if tzinfo is True: + tzinfo = self.tzinfo + + hour = hour if hour is not None else self.hour + minute = minute if minute is not None else self.minute + second = second if second is not None else self.second + microsecond = microsecond if microsecond is not None else self.microsecond + + t = super().replace( + hour, + minute, + second, + microsecond, + tzinfo=cast(Optional[datetime.tzinfo], tzinfo), + fold=fold, + ) + return self.__class__( + t.hour, t.minute, t.second, t.microsecond, tzinfo=t.tzinfo + ) + + def __getnewargs__(self) -> tuple[Time]: + return (self,) + + def _get_state( + self, protocol: SupportsIndex = 3 + ) -> tuple[int, int, int, int, datetime.tzinfo | None]: + tz = self.tzinfo + + return self.hour, self.minute, self.second, self.microsecond, tz + + def __reduce__( + self, + ) -> tuple[type[Time], tuple[int, int, int, int, datetime.tzinfo | None]]: + return self.__reduce_ex__(2) + + def __reduce_ex__( + self, protocol: SupportsIndex + ) -> tuple[type[Time], tuple[int, int, int, int, datetime.tzinfo | None]]: + return self.__class__, self._get_state(protocol) + + +Time.min = Time(0, 0, 0) +Time.max = Time(23, 59, 59, 999999) +Time.resolution = Duration(microseconds=1) diff --git a/src/pendulum/tz/__init__.py b/src/pendulum/tz/__init__.py new file mode 100644 index 0000000..36c2c69 --- /dev/null +++ b/src/pendulum/tz/__init__.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from pathlib import Path +from typing import cast + +from pendulum.tz.local_timezone import get_local_timezone +from pendulum.tz.local_timezone import set_local_timezone +from pendulum.tz.local_timezone import test_local_timezone +from pendulum.tz.timezone import UTC +from pendulum.tz.timezone import FixedTimezone +from pendulum.tz.timezone import Timezone +from pendulum.utils._compat import resources + + +PRE_TRANSITION = "pre" +POST_TRANSITION = "post" +TRANSITION_ERROR = "error" + +_timezones = None + +_tz_cache: dict[int, FixedTimezone] = {} + + +def timezones() -> tuple[str, ...]: + global _timezones + + if _timezones is None: + with cast(Path, resources.files("tzdata").joinpath("zones")).open() as f: + _timezones = tuple(tz.strip() for tz in f.readlines()) + + return _timezones + + +def fixed_timezone(offset: int) -> FixedTimezone: + """ + Return a Timezone instance given its offset in seconds. + """ + if offset in _tz_cache: + return _tz_cache[offset] + + tz = FixedTimezone(offset) + _tz_cache[offset] = tz + + return tz + + +def local_timezone() -> Timezone | FixedTimezone: + """ + Return the local timezone. + """ + return get_local_timezone() + + +__all__ = [ + "UTC", + "Timezone", + "FixedTimezone", + "set_local_timezone", + "get_local_timezone", + "test_local_timezone", + "fixed_timezone", + "local_timezone", + "timezones", +] diff --git a/src/pendulum/tz/data/__init__.py b/src/pendulum/tz/data/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/tz/data/__init__.py diff --git a/src/pendulum/tz/data/windows.py b/src/pendulum/tz/data/windows.py new file mode 100644 index 0000000..e76e6bb --- /dev/null +++ b/src/pendulum/tz/data/windows.py @@ -0,0 +1,140 @@ +from __future__ import annotations + + +windows_timezones = { + "AUS Central Standard Time": "Australia/Darwin", + "AUS Eastern Standard Time": "Australia/Sydney", + "Afghanistan Standard Time": "Asia/Kabul", + "Alaskan Standard Time": "America/Anchorage", + "Aleutian Standard Time": "America/Adak", + "Altai Standard Time": "Asia/Barnaul", + "Arab Standard Time": "Asia/Riyadh", + "Arabian Standard Time": "Asia/Dubai", + "Arabic Standard Time": "Asia/Baghdad", + "Argentina Standard Time": "America/Buenos_Aires", + "Astrakhan Standard Time": "Europe/Astrakhan", + "Atlantic Standard Time": "America/Halifax", + "Aus Central W. Standard Time": "Australia/Eucla", + "Azerbaijan Standard Time": "Asia/Baku", + "Azores Standard Time": "Atlantic/Azores", + "Bahia Standard Time": "America/Bahia", + "Bangladesh Standard Time": "Asia/Dhaka", + "Belarus Standard Time": "Europe/Minsk", + "Bougainville Standard Time": "Pacific/Bougainville", + "Canada Central Standard Time": "America/Regina", + "Cape Verde Standard Time": "Atlantic/Cape_Verde", + "Caucasus Standard Time": "Asia/Yerevan", + "Cen. Australia Standard Time": "Australia/Adelaide", + "Central America Standard Time": "America/Guatemala", + "Central Asia Standard Time": "Asia/Almaty", + "Central Brazilian Standard Time": "America/Cuiaba", + "Central Europe Standard Time": "Europe/Budapest", + "Central European Standard Time": "Europe/Warsaw", + "Central Pacific Standard Time": "Pacific/Guadalcanal", + "Central Standard Time": "America/Chicago", + "Central Standard Time (Mexico)": "America/Mexico_City", + "Chatham Islands Standard Time": "Pacific/Chatham", + "China Standard Time": "Asia/Shanghai", + "Cuba Standard Time": "America/Havana", + "Dateline Standard Time": "Etc/GMT+12", + "E. Africa Standard Time": "Africa/Nairobi", + "E. Australia Standard Time": "Australia/Brisbane", + "E. Europe Standard Time": "Europe/Chisinau", + "E. South America Standard Time": "America/Sao_Paulo", + "Easter Island Standard Time": "Pacific/Easter", + "Eastern Standard Time": "America/New_York", + "Eastern Standard Time (Mexico)": "America/Cancun", + "Egypt Standard Time": "Africa/Cairo", + "Ekaterinburg Standard Time": "Asia/Yekaterinburg", + "FLE Standard Time": "Europe/Kiev", + "Fiji Standard Time": "Pacific/Fiji", + "GMT Standard Time": "Europe/London", + "GTB Standard Time": "Europe/Bucharest", + "Georgian Standard Time": "Asia/Tbilisi", + "Greenland Standard Time": "America/Godthab", + "Greenwich Standard Time": "Atlantic/Reykjavik", + "Haiti Standard Time": "America/Port-au-Prince", + "Hawaiian Standard Time": "Pacific/Honolulu", + "India Standard Time": "Asia/Calcutta", + "Iran Standard Time": "Asia/Tehran", + "Israel Standard Time": "Asia/Jerusalem", + "Jordan Standard Time": "Asia/Amman", + "Kaliningrad Standard Time": "Europe/Kaliningrad", + "Korea Standard Time": "Asia/Seoul", + "Libya Standard Time": "Africa/Tripoli", + "Line Islands Standard Time": "Pacific/Kiritimati", + "Lord Howe Standard Time": "Australia/Lord_Howe", + "Magadan Standard Time": "Asia/Magadan", + "Magallanes Standard Time": "America/Punta_Arenas", + "Marquesas Standard Time": "Pacific/Marquesas", + "Mauritius Standard Time": "Indian/Mauritius", + "Middle East Standard Time": "Asia/Beirut", + "Montevideo Standard Time": "America/Montevideo", + "Morocco Standard Time": "Africa/Casablanca", + "Mountain Standard Time": "America/Denver", + "Mountain Standard Time (Mexico)": "America/Chihuahua", + "Myanmar Standard Time": "Asia/Rangoon", + "N. Central Asia Standard Time": "Asia/Novosibirsk", + "Namibia Standard Time": "Africa/Windhoek", + "Nepal Standard Time": "Asia/Katmandu", + "New Zealand Standard Time": "Pacific/Auckland", + "Newfoundland Standard Time": "America/St_Johns", + "Norfolk Standard Time": "Pacific/Norfolk", + "North Asia East Standard Time": "Asia/Irkutsk", + "North Asia Standard Time": "Asia/Krasnoyarsk", + "North Korea Standard Time": "Asia/Pyongyang", + "Omsk Standard Time": "Asia/Omsk", + "Pacific SA Standard Time": "America/Santiago", + "Pacific Standard Time": "America/Los_Angeles", + "Pacific Standard Time (Mexico)": "America/Tijuana", + "Pakistan Standard Time": "Asia/Karachi", + "Paraguay Standard Time": "America/Asuncion", + "Romance Standard Time": "Europe/Paris", + "Russia Time Zone 10": "Asia/Srednekolymsk", + "Russia Time Zone 11": "Asia/Kamchatka", + "Russia Time Zone 3": "Europe/Samara", + "Russian Standard Time": "Europe/Moscow", + "SA Eastern Standard Time": "America/Cayenne", + "SA Pacific Standard Time": "America/Bogota", + "SA Western Standard Time": "America/La_Paz", + "SE Asia Standard Time": "Asia/Bangkok", + "Saint Pierre Standard Time": "America/Miquelon", + "Sakhalin Standard Time": "Asia/Sakhalin", + "Samoa Standard Time": "Pacific/Apia", + "Sao Tome Standard Time": "Africa/Sao_Tome", + "Saratov Standard Time": "Europe/Saratov", + "Singapore Standard Time": "Asia/Singapore", + "South Africa Standard Time": "Africa/Johannesburg", + "Sri Lanka Standard Time": "Asia/Colombo", + "Sudan Standard Time": "Africa/Khartoum", + "Syria Standard Time": "Asia/Damascus", + "Taipei Standard Time": "Asia/Taipei", + "Tasmania Standard Time": "Australia/Hobart", + "Tocantins Standard Time": "America/Araguaina", + "Tokyo Standard Time": "Asia/Tokyo", + "Tomsk Standard Time": "Asia/Tomsk", + "Tonga Standard Time": "Pacific/Tongatapu", + "Transbaikal Standard Time": "Asia/Chita", + "Turkey Standard Time": "Europe/Istanbul", + "Turks And Caicos Standard Time": "America/Grand_Turk", + "US Eastern Standard Time": "America/Indianapolis", + "US Mountain Standard Time": "America/Phoenix", + "UTC": "Etc/GMT", + "UTC+12": "Etc/GMT-12", + "UTC+13": "Etc/GMT-13", + "UTC-02": "Etc/GMT+2", + "UTC-08": "Etc/GMT+8", + "UTC-09": "Etc/GMT+9", + "UTC-11": "Etc/GMT+11", + "Ulaanbaatar Standard Time": "Asia/Ulaanbaatar", + "Venezuela Standard Time": "America/Caracas", + "Vladivostok Standard Time": "Asia/Vladivostok", + "W. Australia Standard Time": "Australia/Perth", + "W. Central Africa Standard Time": "Africa/Lagos", + "W. Europe Standard Time": "Europe/Berlin", + "W. Mongolia Standard Time": "Asia/Hovd", + "West Asia Standard Time": "Asia/Tashkent", + "West Bank Standard Time": "Asia/Hebron", + "West Pacific Standard Time": "Pacific/Port_Moresby", + "Yakutsk Standard Time": "Asia/Yakutsk", +} diff --git a/src/pendulum/tz/exceptions.py b/src/pendulum/tz/exceptions.py new file mode 100644 index 0000000..d06c133 --- /dev/null +++ b/src/pendulum/tz/exceptions.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from datetime import datetime + + +class TimezoneError(ValueError): + pass + + +class InvalidTimezone(TimezoneError): + pass + + +class NonExistingTime(TimezoneError): + message = "The datetime {} does not exist." + + def __init__(self, dt: datetime) -> None: + message = self.message.format(dt) + + super().__init__(message) + + +class AmbiguousTime(TimezoneError): + message = "The datetime {} is ambiguous." + + def __init__(self, dt: datetime) -> None: + message = self.message.format(dt) + + super().__init__(message) diff --git a/src/pendulum/tz/local_timezone.py b/src/pendulum/tz/local_timezone.py new file mode 100644 index 0000000..3cb488c --- /dev/null +++ b/src/pendulum/tz/local_timezone.py @@ -0,0 +1,264 @@ +from __future__ import annotations + +import contextlib +import os +import re +import sys +import warnings + +from contextlib import contextmanager +from typing import Iterator +from typing import cast + +from pendulum.tz.exceptions import InvalidTimezone +from pendulum.tz.timezone import UTC +from pendulum.tz.timezone import FixedTimezone +from pendulum.tz.timezone import Timezone + + +if sys.platform == "win32": + import winreg + +_mock_local_timezone = None +_local_timezone = None + + +def get_local_timezone() -> Timezone | FixedTimezone: + global _local_timezone + + if _mock_local_timezone is not None: + return _mock_local_timezone + + if _local_timezone is None: + tz = _get_system_timezone() + + _local_timezone = tz + + return _local_timezone + + +def set_local_timezone(mock: str | Timezone | None = None) -> None: + global _mock_local_timezone + + _mock_local_timezone = mock + + +@contextmanager +def test_local_timezone(mock: Timezone) -> Iterator[None]: + set_local_timezone(mock) + + yield + + set_local_timezone() + + +def _get_system_timezone() -> Timezone: + if sys.platform == "win32": + return _get_windows_timezone() + elif "darwin" in sys.platform: + return _get_darwin_timezone() + + return _get_unix_timezone() + + +if sys.platform == "win32": + + def _get_windows_timezone() -> Timezone: + from pendulum.tz.data.windows import windows_timezones + + # Windows is special. It has unique time zone names (in several + # meanings of the word) available, but unfortunately, they can be + # translated to the language of the operating system, so we need to + # do a backwards lookup, by going through all time zones and see which + # one matches. + handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) + + tz_local_key_name = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" + localtz = winreg.OpenKey(handle, tz_local_key_name) + + timezone_info = {} + size = winreg.QueryInfoKey(localtz)[1] + for i in range(size): + data = winreg.EnumValue(localtz, i) + timezone_info[data[0]] = data[1] + + localtz.Close() + + if "TimeZoneKeyName" in timezone_info: + # Windows 7 (and Vista?) + + # For some reason this returns a string with loads of NUL bytes at + # least on some systems. I don't know if this is a bug somewhere, I + # just work around it. + tzkeyname = timezone_info["TimeZoneKeyName"].split("\x00", 1)[0] + else: + # Windows 2000 or XP + + # This is the localized name: + tzwin = timezone_info["StandardName"] + + # Open the list of timezones to look up the real name: + tz_key_name = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" + tzkey = winreg.OpenKey(handle, tz_key_name) + + # Now, match this value to Time Zone information + tzkeyname = None + for i in range(winreg.QueryInfoKey(tzkey)[0]): + subkey = winreg.EnumKey(tzkey, i) + sub = winreg.OpenKey(tzkey, subkey) + + info = {} + size = winreg.QueryInfoKey(sub)[1] + for i in range(size): + data = winreg.EnumValue(sub, i) + info[data[0]] = data[1] + + sub.Close() + with contextlib.suppress(KeyError): + # This timezone didn't have proper configuration. + # Ignore it. + if info["Std"] == tzwin: + tzkeyname = subkey + break + + tzkey.Close() + handle.Close() + + if tzkeyname is None: + raise LookupError("Can not find Windows timezone configuration") + + timezone = windows_timezones.get(tzkeyname) + if timezone is None: + # Nope, that didn't work. Try adding "Standard Time", + # it seems to work a lot of times: + timezone = windows_timezones.get(tzkeyname + " Standard Time") + + # Return what we have. + if timezone is None: + raise LookupError("Unable to find timezone " + tzkeyname) + + return Timezone(timezone) + +else: + + def _get_windows_timezone() -> Timezone: + raise NotImplementedError + + +def _get_darwin_timezone() -> Timezone: + # link will be something like /usr/share/zoneinfo/America/Los_Angeles. + link = os.readlink("/etc/localtime") + tzname = link[link.rfind("zoneinfo/") + 9 :] + + return Timezone(tzname) + + +def _get_unix_timezone(_root: str = "/") -> Timezone: + tzenv = os.environ.get("TZ") + if tzenv: + with contextlib.suppress(ValueError): + return _tz_from_env(tzenv) + + # Now look for distribution specific configuration files + # that contain the timezone name. + tzpath = os.path.join(_root, "etc/timezone") + if os.path.isfile(tzpath): + with open(tzpath, "rb") as tzfile: + tzfile_data = tzfile.read() + + # Issue #3 was that /etc/timezone was a zoneinfo file. + # That's a misconfiguration, but we need to handle it gracefully: + if tzfile_data[:5] != b"TZif2": + etctz = tzfile_data.strip().decode() + # Get rid of host definitions and comments: + if " " in etctz: + etctz, dummy = etctz.split(" ", 1) + if "#" in etctz: + etctz, dummy = etctz.split("#", 1) + + return Timezone(etctz.replace(" ", "_")) + + # CentOS has a ZONE setting in /etc/sysconfig/clock, + # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and + # Gentoo has a TIMEZONE setting in /etc/conf.d/clock + # We look through these files for a timezone: + zone_re = re.compile(r'\s*ZONE\s*=\s*"') + timezone_re = re.compile(r'\s*TIMEZONE\s*=\s*"') + end_re = re.compile('"') + + for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"): + tzpath = os.path.join(_root, filename) + if not os.path.isfile(tzpath): + continue + + with open(tzpath) as tzfile: + data = tzfile.readlines() + + for line in data: + # Look for the ZONE= setting. + match = zone_re.match(line) + if match is None: + # No ZONE= setting. Look for the TIMEZONE= setting. + match = timezone_re.match(line) + + if match is not None: + # Some setting existed + line = line[match.end() :] + etctz = line[ + : cast( + re.Match, end_re.search(line) # type: ignore[type-arg] + ).start() + ] + + parts = list(reversed(etctz.replace(" ", "_").split(os.path.sep))) + tzpath_parts: list[str] = [] + while parts: + tzpath_parts.insert(0, parts.pop(0)) + + with contextlib.suppress(InvalidTimezone): + return Timezone(os.path.join(*tzpath_parts)) + + # systemd distributions use symlinks that include the zone name, + # see manpage of localtime(5) and timedatectl(1) + tzpath = os.path.join(_root, "etc", "localtime") + if os.path.isfile(tzpath) and os.path.islink(tzpath): + parts = list( + reversed(os.path.realpath(tzpath).replace(" ", "_").split(os.path.sep)) + ) + tzpath_parts: list[str] = [] # type: ignore[no-redef] + while parts: + tzpath_parts.insert(0, parts.pop(0)) + with contextlib.suppress(InvalidTimezone): + return Timezone(os.path.join(*tzpath_parts)) + + # No explicit setting existed. Use localtime + for filename in ("etc/localtime", "usr/local/etc/localtime"): + tzpath = os.path.join(_root, filename) + + if not os.path.isfile(tzpath): + continue + + with open(tzpath, "rb") as f: + return Timezone.from_file(f) + + warnings.warn( + "Unable not find any timezone configuration, defaulting to UTC.", stacklevel=1 + ) + + return UTC + + +def _tz_from_env(tzenv: str) -> Timezone: + if tzenv[0] == ":": + tzenv = tzenv[1:] + + # TZ specifies a file + if os.path.isfile(tzenv): + with open(tzenv, "rb") as f: + return Timezone.from_file(f) + + # TZ specifies a zoneinfo zone. + try: + return Timezone(tzenv) + except ValueError: + raise diff --git a/src/pendulum/tz/timezone.py b/src/pendulum/tz/timezone.py new file mode 100644 index 0000000..0c6eb8d --- /dev/null +++ b/src/pendulum/tz/timezone.py @@ -0,0 +1,239 @@ +# mypy: no-warn-redundant-casts +from __future__ import annotations + +import datetime as _datetime + +from abc import ABC +from abc import abstractmethod +from typing import TYPE_CHECKING +from typing import TypeVar +from typing import cast + +from pendulum.tz.exceptions import AmbiguousTime +from pendulum.tz.exceptions import InvalidTimezone +from pendulum.tz.exceptions import NonExistingTime +from pendulum.utils._compat import zoneinfo + + +if TYPE_CHECKING: + from typing_extensions import Self + +POST_TRANSITION = "post" +PRE_TRANSITION = "pre" +TRANSITION_ERROR = "error" + + +_DT = TypeVar("_DT", bound=_datetime.datetime) + + +class PendulumTimezone(ABC): + @property + @abstractmethod + def name(self) -> str: + raise NotImplementedError + + @abstractmethod + def convert(self, dt: _DT, raise_on_unknown_times: bool = False) -> _DT: + raise NotImplementedError + + @abstractmethod + def datetime( + self, + year: int, + month: int, + day: int, + hour: int = 0, + minute: int = 0, + second: int = 0, + microsecond: int = 0, + ) -> _datetime.datetime: + raise NotImplementedError + + +class Timezone(zoneinfo.ZoneInfo, PendulumTimezone): + """ + Represents a named timezone. + + The accepted names are those provided by the IANA time zone database. + + >>> from pendulum.tz.timezone import Timezone + >>> tz = Timezone('Europe/Paris') + """ + + def __new__(cls, key: str) -> Self: + try: + return super().__new__(cls, key) # type: ignore[call-arg] + except zoneinfo.ZoneInfoNotFoundError: + raise InvalidTimezone(key) + + @property + def name(self) -> str: + return self.key + + def convert(self, dt: _DT, raise_on_unknown_times: bool = False) -> _DT: + """ + Converts a datetime in the current timezone. + + If the datetime is naive, it will be normalized. + + >>> from datetime import datetime + >>> from pendulum import timezone + >>> paris = timezone('Europe/Paris') + >>> dt = datetime(2013, 3, 31, 2, 30, fold=1) + >>> in_paris = paris.convert(dt) + >>> in_paris.isoformat() + '2013-03-31T03:30:00+02:00' + + If the datetime is aware, it will be properly converted. + + >>> new_york = timezone('America/New_York') + >>> in_new_york = new_york.convert(in_paris) + >>> in_new_york.isoformat() + '2013-03-30T21:30:00-04:00' + """ + + if dt.tzinfo is None: + # Technically, utcoffset() can return None, but none of the zone information + # in tzdata sets _tti_before to None. This can be checked with the following + # code: + # + # >>> import zoneinfo + # >>> from zoneinfo._zoneinfo import ZoneInfo + # + # >>> for tzname in zoneinfo.available_timezones(): + # >>> if ZoneInfo(tzname)._tti_before is None: + # >>> print(tzname) + + offset_before = cast( + _datetime.timedelta, + (self.utcoffset(dt.replace(fold=0)) if dt.fold else self.utcoffset(dt)), + ) + offset_after = cast( + _datetime.timedelta, + (self.utcoffset(dt) if dt.fold else self.utcoffset(dt.replace(fold=1))), + ) + + if offset_after > offset_before: + # Skipped time + if raise_on_unknown_times: + raise NonExistingTime(dt) + + dt = cast( + _DT, + dt + + ( + (offset_after - offset_before) + if dt.fold + else (offset_before - offset_after) + ), + ) + elif offset_before > offset_after and raise_on_unknown_times: + # Repeated time + raise AmbiguousTime(dt) + + return dt.replace(tzinfo=self) + + return cast(_DT, dt.astimezone(self)) + + def datetime( + self, + year: int, + month: int, + day: int, + hour: int = 0, + minute: int = 0, + second: int = 0, + microsecond: int = 0, + ) -> _datetime.datetime: + """ + Return a normalized datetime for the current timezone. + """ + return self.convert( + _datetime.datetime( + year, month, day, hour, minute, second, microsecond, fold=1 + ) + ) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}('{self.name}')" + + +class FixedTimezone(_datetime.tzinfo, PendulumTimezone): + def __init__(self, offset: int, name: str | None = None) -> None: + sign = "-" if offset < 0 else "+" + + minutes = offset / 60 + hour, minute = divmod(abs(int(minutes)), 60) + + if not name: + name = f"{sign}{hour:02d}:{minute:02d}" + + self._name = name + self._offset = offset + self._utcoffset = _datetime.timedelta(seconds=offset) + + @property + def name(self) -> str: + return self._name + + def convert(self, dt: _DT, raise_on_unknown_times: bool = False) -> _DT: + if dt.tzinfo is None: + return dt.__class__( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=self, + fold=0, + ) + + return cast(_DT, dt.astimezone(self)) + + def datetime( + self, + year: int, + month: int, + day: int, + hour: int = 0, + minute: int = 0, + second: int = 0, + microsecond: int = 0, + ) -> _datetime.datetime: + return self.convert( + _datetime.datetime( + year, month, day, hour, minute, second, microsecond, fold=1 + ) + ) + + @property + def offset(self) -> int: + return self._offset + + def utcoffset(self, dt: _datetime.datetime | None) -> _datetime.timedelta: + return self._utcoffset + + def dst(self, dt: _datetime.datetime | None) -> _datetime.timedelta: + return _datetime.timedelta() + + def fromutc(self, dt: _datetime.datetime) -> _datetime.datetime: + # Use the stdlib datetime's add method to avoid infinite recursion + return (_datetime.datetime.__add__(dt, self._utcoffset)).replace(tzinfo=self) + + def tzname(self, dt: _datetime.datetime | None) -> str | None: + return self._name + + def __getinitargs__(self) -> tuple[int, str]: + return self._offset, self._name + + def __repr__(self) -> str: + name = "" + if self._name: + name = f', name="{self._name}"' + + return f"{self.__class__.__name__}({self._offset}{name})" + + +UTC = Timezone("UTC") diff --git a/src/pendulum/utils/__init__.py b/src/pendulum/utils/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pendulum/utils/__init__.py diff --git a/src/pendulum/utils/_compat.py b/src/pendulum/utils/_compat.py new file mode 100644 index 0000000..e4dfedf --- /dev/null +++ b/src/pendulum/utils/_compat.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +import sys + +from pendulum.utils import _zoneinfo as zoneinfo + + +PYPY = hasattr(sys, "pypy_version_info") + +if sys.version_info < (3, 9): + import importlib_resources as resources +else: + from importlib import resources + +__all__ = ["resources", "zoneinfo"] diff --git a/src/pendulum/utils/_zoneinfo.py b/src/pendulum/utils/_zoneinfo.py new file mode 100644 index 0000000..e9f0251 --- /dev/null +++ b/src/pendulum/utils/_zoneinfo.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import sys + +from typing import TYPE_CHECKING + + +if sys.version_info < (3, 9): + # Works around https://github.com/pganssle/zoneinfo/issues/125 + from backports.zoneinfo import TZPATH + from backports.zoneinfo import InvalidTZPathWarning + from backports.zoneinfo import ZoneInfoNotFoundError + from backports.zoneinfo import available_timezones + from backports.zoneinfo import reset_tzpath + + if TYPE_CHECKING: + from collections.abc import Iterable + from datetime import datetime + from datetime import timedelta + from datetime import tzinfo + from typing import Any + from typing import Protocol + + from typing_extensions import Self + + class _IOBytes(Protocol): + def read(self, __size: int) -> bytes: + ... + + def seek(self, __size: int, __whence: int = ...) -> Any: + ... + + class ZoneInfo(tzinfo): + @property + def key(self) -> str: + ... + + def __init__(self, key: str) -> None: + ... + + @classmethod + def no_cache(cls, key: str) -> Self: + ... + + @classmethod + def from_file(cls, __fobj: _IOBytes, key: str | None = ...) -> Self: + ... + + @classmethod + def clear_cache(cls, *, only_keys: Iterable[str] | None = ...) -> None: + ... + + def tzname(self, __dt: datetime | None) -> str | None: + ... + + def utcoffset(self, __dt: datetime | None) -> timedelta | None: + ... + + def dst(self, __dt: datetime | None) -> timedelta | None: + ... + + else: + from backports.zoneinfo import ZoneInfo + +else: + from zoneinfo import TZPATH + from zoneinfo import InvalidTZPathWarning + from zoneinfo import ZoneInfo + from zoneinfo import ZoneInfoNotFoundError + from zoneinfo import available_timezones + from zoneinfo import reset_tzpath + +__all__ = [ + "ZoneInfo", + "reset_tzpath", + "available_timezones", + "TZPATH", + "ZoneInfoNotFoundError", + "InvalidTZPathWarning", +] |