summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/pendulum/__init__.py402
-rw-r--r--src/pendulum/__version__.py4
-rw-r--r--src/pendulum/_helpers.py335
-rw-r--r--src/pendulum/_pendulum.pyi40
-rw-r--r--src/pendulum/constants.py102
-rw-r--r--src/pendulum/date.py760
-rw-r--r--src/pendulum/datetime.py1404
-rw-r--r--src/pendulum/day.py13
-rw-r--r--src/pendulum/duration.py533
-rw-r--r--src/pendulum/exceptions.py13
-rw-r--r--src/pendulum/formatting/__init__.py6
-rw-r--r--src/pendulum/formatting/difference_formatter.py144
-rw-r--r--src/pendulum/formatting/formatter.py698
-rw-r--r--src/pendulum/helpers.py221
-rw-r--r--src/pendulum/interval.py455
-rw-r--r--src/pendulum/locales/__init__.py0
-rw-r--r--src/pendulum/locales/cs/__init__.py0
-rw-r--r--src/pendulum/locales/cs/custom.py25
-rw-r--r--src/pendulum/locales/cs/locale.py274
-rw-r--r--src/pendulum/locales/da/__init__.py0
-rw-r--r--src/pendulum/locales/da/custom.py20
-rw-r--r--src/pendulum/locales/da/locale.py155
-rw-r--r--src/pendulum/locales/de/__init__.py0
-rw-r--r--src/pendulum/locales/de/custom.py38
-rw-r--r--src/pendulum/locales/de/locale.py152
-rw-r--r--src/pendulum/locales/en/__init__.py0
-rw-r--r--src/pendulum/locales/en/custom.py25
-rw-r--r--src/pendulum/locales/en/locale.py158
-rw-r--r--src/pendulum/locales/en_gb/__init__.py0
-rw-r--r--src/pendulum/locales/en_gb/custom.py25
-rw-r--r--src/pendulum/locales/en_gb/locale.py240
-rw-r--r--src/pendulum/locales/en_us/__init__.py0
-rw-r--r--src/pendulum/locales/en_us/custom.py25
-rw-r--r--src/pendulum/locales/en_us/locale.py240
-rw-r--r--src/pendulum/locales/es/__init__.py0
-rw-r--r--src/pendulum/locales/es/custom.py25
-rw-r--r--src/pendulum/locales/es/locale.py149
-rw-r--r--src/pendulum/locales/fa/__init__.py0
-rw-r--r--src/pendulum/locales/fa/custom.py20
-rw-r--r--src/pendulum/locales/fa/locale.py143
-rw-r--r--src/pendulum/locales/fo/__init__.py0
-rw-r--r--src/pendulum/locales/fo/custom.py22
-rw-r--r--src/pendulum/locales/fo/locale.py140
-rw-r--r--src/pendulum/locales/fr/__init__.py0
-rw-r--r--src/pendulum/locales/fr/custom.py25
-rw-r--r--src/pendulum/locales/fr/locale.py141
-rw-r--r--src/pendulum/locales/he/__init__.py0
-rw-r--r--src/pendulum/locales/he/custom.py25
-rw-r--r--src/pendulum/locales/he/locale.py277
-rw-r--r--src/pendulum/locales/id/__init__.py0
-rw-r--r--src/pendulum/locales/id/custom.py21
-rw-r--r--src/pendulum/locales/id/locale.py149
-rw-r--r--src/pendulum/locales/it/__init__.py0
-rw-r--r--src/pendulum/locales/it/custom.py25
-rw-r--r--src/pendulum/locales/it/locale.py153
-rw-r--r--src/pendulum/locales/ja/__init__.py0
-rw-r--r--src/pendulum/locales/ja/custom.py23
-rw-r--r--src/pendulum/locales/ja/locale.py202
-rw-r--r--src/pendulum/locales/ko/__init__.py0
-rw-r--r--src/pendulum/locales/ko/custom.py20
-rw-r--r--src/pendulum/locales/ko/locale.py113
-rw-r--r--src/pendulum/locales/locale.py100
-rw-r--r--src/pendulum/locales/lt/__init__.py0
-rw-r--r--src/pendulum/locales/lt/custom.py120
-rw-r--r--src/pendulum/locales/lt/locale.py263
-rw-r--r--src/pendulum/locales/nb/__init__.py0
-rw-r--r--src/pendulum/locales/nb/custom.py22
-rw-r--r--src/pendulum/locales/nb/locale.py158
-rw-r--r--src/pendulum/locales/nl/__init__.py0
-rw-r--r--src/pendulum/locales/nl/custom.py25
-rw-r--r--src/pendulum/locales/nl/locale.py142
-rw-r--r--src/pendulum/locales/nn/__init__.py0
-rw-r--r--src/pendulum/locales/nn/custom.py22
-rw-r--r--src/pendulum/locales/nn/locale.py149
-rw-r--r--src/pendulum/locales/pl/__init__.py0
-rw-r--r--src/pendulum/locales/pl/custom.py23
-rw-r--r--src/pendulum/locales/pl/locale.py287
-rw-r--r--src/pendulum/locales/pt_br/__init__.py0
-rw-r--r--src/pendulum/locales/pt_br/custom.py20
-rw-r--r--src/pendulum/locales/pt_br/locale.py151
-rw-r--r--src/pendulum/locales/ru/__init__.py0
-rw-r--r--src/pendulum/locales/ru/custom.py22
-rw-r--r--src/pendulum/locales/ru/locale.py278
-rw-r--r--src/pendulum/locales/sk/__init__.py0
-rw-r--r--src/pendulum/locales/sk/custom.py22
-rw-r--r--src/pendulum/locales/sk/locale.py274
-rw-r--r--src/pendulum/locales/sv/__init__.py0
-rw-r--r--src/pendulum/locales/sv/custom.py22
-rw-r--r--src/pendulum/locales/sv/locale.py230
-rw-r--r--src/pendulum/locales/tr/__init__.py0
-rw-r--r--src/pendulum/locales/tr/custom.py24
-rw-r--r--src/pendulum/locales/tr/locale.py225
-rw-r--r--src/pendulum/locales/zh/__init__.py0
-rw-r--r--src/pendulum/locales/zh/custom.py20
-rw-r--r--src/pendulum/locales/zh/locale.py121
-rw-r--r--src/pendulum/mixins/__init__.py0
-rw-r--r--src/pendulum/mixins/default.py37
-rw-r--r--src/pendulum/parser.py128
-rw-r--r--src/pendulum/parsing/__init__.py235
-rw-r--r--src/pendulum/parsing/exceptions/__init__.py5
-rw-r--r--src/pendulum/parsing/iso8601.py453
-rw-r--r--src/pendulum/py.typed0
-rw-r--r--src/pendulum/testing/__init__.py0
-rw-r--r--src/pendulum/testing/traveller.py172
-rw-r--r--src/pendulum/time.py321
-rw-r--r--src/pendulum/tz/__init__.py64
-rw-r--r--src/pendulum/tz/data/__init__.py0
-rw-r--r--src/pendulum/tz/data/windows.py140
-rw-r--r--src/pendulum/tz/exceptions.py33
-rw-r--r--src/pendulum/tz/local_timezone.py264
-rw-r--r--src/pendulum/tz/timezone.py239
-rw-r--r--src/pendulum/utils/__init__.py0
-rw-r--r--src/pendulum/utils/_compat.py15
-rw-r--r--src/pendulum/utils/_zoneinfo.py80
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",
+]