summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2021-01-30 08:13:47 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2021-01-30 08:13:47 +0000
commit1199780155f666b6806d563a29d093a251664009 (patch)
tree68716d9c1ee3205f474a04d74d5653eddf94a9f2
parentInitial commit. (diff)
downloadpendulum-1199780155f666b6806d563a29d093a251664009.tar.xz
pendulum-1199780155f666b6806d563a29d093a251664009.zip
Adding upstream version 2.1.2.upstream/2.1.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--LICENSE20
-rw-r--r--PKG-INFO251
-rw-r--r--README.rst224
-rw-r--r--build.py87
-rw-r--r--pendulum/__init__.py315
-rw-r--r--pendulum/__version__.py1
-rw-r--r--pendulum/_extensions/__init__.py0
-rw-r--r--pendulum/_extensions/_helpers.c930
-rw-r--r--pendulum/_extensions/helpers.py358
-rw-r--r--pendulum/constants.py107
-rw-r--r--pendulum/date.py891
-rw-r--r--pendulum/datetime.py1563
-rw-r--r--pendulum/duration.py479
-rw-r--r--pendulum/exceptions.py6
-rw-r--r--pendulum/formatting/__init__.py4
-rw-r--r--pendulum/formatting/difference_formatter.py153
-rw-r--r--pendulum/formatting/formatter.py685
-rw-r--r--pendulum/helpers.py224
-rw-r--r--pendulum/locales/__init__.py0
-rw-r--r--pendulum/locales/da/__init__.py0
-rw-r--r--pendulum/locales/da/custom.py22
-rw-r--r--pendulum/locales/da/locale.py150
-rw-r--r--pendulum/locales/de/__init__.py0
-rw-r--r--pendulum/locales/de/custom.py40
-rw-r--r--pendulum/locales/de/locale.py147
-rw-r--r--pendulum/locales/en/__init__.py0
-rw-r--r--pendulum/locales/en/custom.py27
-rw-r--r--pendulum/locales/en/locale.py153
-rw-r--r--pendulum/locales/es/__init__.py0
-rw-r--r--pendulum/locales/es/custom.py27
-rw-r--r--pendulum/locales/es/locale.py144
-rw-r--r--pendulum/locales/fa/__init__.py0
-rw-r--r--pendulum/locales/fa/custom.py22
-rw-r--r--pendulum/locales/fa/locale.py138
-rw-r--r--pendulum/locales/fo/__init__.py0
-rw-r--r--pendulum/locales/fo/custom.py24
-rw-r--r--pendulum/locales/fo/locale.py135
-rw-r--r--pendulum/locales/fr/__init__.py1
-rw-r--r--pendulum/locales/fr/custom.py27
-rw-r--r--pendulum/locales/fr/locale.py136
-rw-r--r--pendulum/locales/id/__init__.py0
-rw-r--r--pendulum/locales/id/custom.py23
-rw-r--r--pendulum/locales/id/locale.py144
-rw-r--r--pendulum/locales/it/__init__.py0
-rw-r--r--pendulum/locales/it/custom.py27
-rw-r--r--pendulum/locales/it/locale.py148
-rw-r--r--pendulum/locales/ko/__init__.py0
-rw-r--r--pendulum/locales/ko/custom.py22
-rw-r--r--pendulum/locales/ko/locale.py108
-rw-r--r--pendulum/locales/locale.py104
-rw-r--r--pendulum/locales/lt/__init__.py0
-rw-r--r--pendulum/locales/lt/custom.py122
-rw-r--r--pendulum/locales/lt/locale.py258
-rw-r--r--pendulum/locales/nb/__init__.py0
-rw-r--r--pendulum/locales/nb/custom.py24
-rw-r--r--pendulum/locales/nb/locale.py153
-rw-r--r--pendulum/locales/nl/__init__.py0
-rw-r--r--pendulum/locales/nl/custom.py27
-rw-r--r--pendulum/locales/nl/locale.py137
-rw-r--r--pendulum/locales/nn/__init__.py0
-rw-r--r--pendulum/locales/nn/custom.py24
-rw-r--r--pendulum/locales/nn/locale.py144
-rw-r--r--pendulum/locales/pl/__init__.py0
-rw-r--r--pendulum/locales/pl/custom.py25
-rw-r--r--pendulum/locales/pl/locale.py282
-rw-r--r--pendulum/locales/pt_br/__init__.py0
-rw-r--r--pendulum/locales/pt_br/custom.py22
-rw-r--r--pendulum/locales/pt_br/locale.py146
-rw-r--r--pendulum/locales/ru/__init__.py0
-rw-r--r--pendulum/locales/ru/custom.py24
-rw-r--r--pendulum/locales/ru/locale.py273
-rw-r--r--pendulum/locales/zh/__init__.py0
-rw-r--r--pendulum/locales/zh/custom.py22
-rw-r--r--pendulum/locales/zh/locale.py116
-rw-r--r--pendulum/mixins/__init__.py1
-rw-r--r--pendulum/mixins/default.py43
-rw-r--r--pendulum/parser.py121
-rw-r--r--pendulum/parsing/__init__.py234
-rw-r--r--pendulum/parsing/_iso8601.c1371
-rw-r--r--pendulum/parsing/exceptions/__init__.py3
-rw-r--r--pendulum/parsing/iso8601.py447
-rw-r--r--pendulum/period.py390
-rw-r--r--pendulum/py.typed0
-rw-r--r--pendulum/time.py284
-rw-r--r--pendulum/tz/__init__.py60
-rw-r--r--pendulum/tz/data/__init__.py0
-rw-r--r--pendulum/tz/data/windows.py137
-rw-r--r--pendulum/tz/exceptions.py23
-rw-r--r--pendulum/tz/local_timezone.py257
-rw-r--r--pendulum/tz/timezone.py377
-rw-r--r--pendulum/tz/zoneinfo/__init__.py16
-rw-r--r--pendulum/tz/zoneinfo/exceptions.py18
-rw-r--r--pendulum/tz/zoneinfo/posix_timezone.py270
-rw-r--r--pendulum/tz/zoneinfo/reader.py224
-rw-r--r--pendulum/tz/zoneinfo/timezone.py128
-rw-r--r--pendulum/tz/zoneinfo/transition.py77
-rw-r--r--pendulum/tz/zoneinfo/transition_type.py35
-rw-r--r--pendulum/utils/__init__.py0
-rw-r--r--pendulum/utils/_compat.py54
-rw-r--r--pyproject.toml81
100 files changed, 14517 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b9cd466
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2015 Sébastien Eustace
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..ff329a6
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,251 @@
+Metadata-Version: 2.1
+Name: pendulum
+Version: 2.1.2
+Summary: Python datetimes made easy
+Home-page: https://pendulum.eustace.io
+License: MIT
+Keywords: datetime,date,time
+Author: Sébastien Eustace
+Author-email: sebastien@eustace.io
+Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Requires-Dist: python-dateutil (>=2.6,<3.0)
+Requires-Dist: pytzdata (>=2020.1)
+Requires-Dist: typing (>=3.6,<4.0); python_version < "3.5"
+Project-URL: Documentation, https://pendulum.eustace.io/docs
+Project-URL: Repository, https://github.com/sdispater/pendulum
+Description-Content-Type: text/x-rst
+
+Pendulum
+########
+
+.. image:: https://img.shields.io/pypi/v/pendulum.svg
+ :target: https://pypi.python.org/pypi/pendulum
+
+.. image:: https://img.shields.io/pypi/l/pendulum.svg
+ :target: https://pypi.python.org/pypi/pendulum
+
+.. image:: https://img.shields.io/codecov/c/github/sdispater/pendulum/master.svg
+ :target: https://codecov.io/gh/sdispater/pendulum/branch/master
+
+.. image:: https://travis-ci.org/sdispater/pendulum.svg
+ :alt: Pendulum Build status
+ :target: https://travis-ci.org/sdispater/pendulum
+
+Python datetimes made easy.
+
+Supports Python **2.7** and **3.4+**.
+
+
+.. code-block:: python
+
+ >>> import pendulum
+
+ >>> now_in_paris = pendulum.now('Europe/Paris')
+ >>> now_in_paris
+ '2016-07-04T00:49:58.502116+02:00'
+
+ # Seamless timezone switching
+ >>> now_in_paris.in_timezone('UTC')
+ '2016-07-03T22:49:58.502116+00:00'
+
+ >>> tomorrow = pendulum.now().add(days=1)
+ >>> last_week = pendulum.now().subtract(weeks=1)
+
+ >>> past = pendulum.now().subtract(minutes=2)
+ >>> past.diff_for_humans()
+ >>> '2 minutes ago'
+
+ >>> delta = past - last_week
+ >>> delta.hours
+ 23
+ >>> delta.in_words(locale='en')
+ '6 days 23 hours 58 minutes'
+
+ # Proper handling of datetime normalization
+ >>> pendulum.datetime(2013, 3, 31, 2, 30, tz='Europe/Paris')
+ '2013-03-31T03:30:00+02:00' # 2:30 does not exist (Skipped time)
+
+ # Proper handling of dst transitions
+ >>> just_before = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, tz='Europe/Paris')
+ '2013-03-31T01:59:59.999999+01:00'
+ >>> just_before.add(microseconds=1)
+ '2013-03-31T03:00:00+02:00'
+
+
+Why Pendulum?
+=============
+
+Native ``datetime`` instances are enough for basic cases but when you face more complex use-cases
+they often show limitations and are not so intuitive to work with.
+``Pendulum`` provides a cleaner and more easy to use API while still relying on the standard library.
+So it's still ``datetime`` but better.
+
+Unlike other datetime libraries for Python, Pendulum is a drop-in replacement
+for the standard ``datetime`` class (it inherits from it), so, basically, you can replace all your ``datetime``
+instances by ``DateTime`` instances in you code (exceptions exist for libraries that check
+the type of the objects by using the ``type`` function like ``sqlite3`` or ``PyMySQL`` for instance).
+
+It also removes the notion of naive datetimes: each ``Pendulum`` instance is timezone-aware
+and by default in ``UTC`` for ease of use.
+
+Pendulum also improves the standard ``timedelta`` class by providing more intuitive methods and properties.
+
+
+Why not Arrow?
+==============
+
+Arrow is the most popular datetime library for Python right now, however its behavior
+and API can be erratic and unpredictable. The ``get()`` method can receive pretty much anything
+and it will try its best to return something while silently failing to handle some cases:
+
+.. code-block:: python
+
+ arrow.get('2016-1-17')
+ # <Arrow [2016-01-01T00:00:00+00:00]>
+
+ pendulum.parse('2016-1-17')
+ # <Pendulum [2016-01-17T00:00:00+00:00]>
+
+ arrow.get('20160413')
+ # <Arrow [1970-08-22T08:06:53+00:00]>
+
+ pendulum.parse('20160413')
+ # <Pendulum [2016-04-13T00:00:00+00:00]>
+
+ arrow.get('2016-W07-5')
+ # <Arrow [2016-01-01T00:00:00+00:00]>
+
+ pendulum.parse('2016-W07-5')
+ # <Pendulum [2016-02-19T00:00:00+00:00]>
+
+ # Working with DST
+ just_before = arrow.Arrow(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris')
+ just_after = just_before.replace(microseconds=1)
+ '2013-03-31T02:00:00+02:00'
+ # Should be 2013-03-31T03:00:00+02:00
+
+ (just_after.to('utc') - just_before.to('utc')).total_seconds()
+ -3599.999999
+ # Should be 1e-06
+
+ just_before = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris')
+ just_after = just_before.add(microseconds=1)
+ '2013-03-31T03:00:00+02:00'
+
+ (just_after.in_timezone('utc') - just_before.in_timezone('utc')).total_seconds()
+ 1e-06
+
+Those are a few examples showing that Arrow cannot always be trusted to have a consistent
+behavior with the data you are passing to it.
+
+
+Limitations
+===========
+
+Even though the ``DateTime`` class is a subclass of ``datetime`` there are some rare cases where
+it can't replace the native class directly. Here is a list (non-exhaustive) of the reported cases with
+a possible solution, if any:
+
+* ``sqlite3`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter:
+
+.. code-block:: python
+
+ from pendulum import DateTime
+ from sqlite3 import register_adapter
+
+ register_adapter(DateTime, lambda val: val.isoformat(' '))
+
+* ``mysqlclient`` (former ``MySQLdb``) and ``PyMySQL`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter:
+
+.. code-block:: python
+
+ import MySQLdb.converters
+ import pymysql.converters
+
+ from pendulum import DateTime
+
+ MySQLdb.converters.conversions[DateTime] = MySQLdb.converters.DateTime2literal
+ pymysql.converters.conversions[DateTime] = pymysql.converters.escape_datetime
+
+* ``django`` will use the ``isoformat()`` method to store datetimes in the database. However since ``pendulum`` is always timezone aware the offset information will always be returned by ``isoformat()`` raising an error, at least for MySQL databases. To work around it you can either create your own ``DateTimeField`` or use the previous workaround for ``MySQLdb``:
+
+.. code-block:: python
+
+ from django.db.models import DateTimeField as BaseDateTimeField
+ from pendulum import DateTime
+
+
+ class DateTimeField(BaseDateTimeField):
+
+ def value_to_string(self, obj):
+ val = self.value_from_object(obj)
+
+ if isinstance(value, DateTime):
+ return value.to_datetime_string()
+
+ return '' if val is None else val.isoformat()
+
+
+Resources
+=========
+
+* `Official Website <https://pendulum.eustace.io>`_
+* `Documentation <https://pendulum.eustace.io/docs/>`_
+* `Issue Tracker <https://github.com/sdispater/pendulum/issues>`_
+
+
+Contributing
+============
+
+Contributions are welcome, especially with localization.
+
+Getting started
+---------------
+
+To work on the Pendulum codebase, you'll want to clone the project locally
+and install the required depedendencies via `poetry <https://poetry.eustace.io>`_.
+
+.. code-block:: bash
+
+ $ git clone git@github.com:sdispater/pendulum.git
+ $ poetry install
+
+Localization
+------------
+
+If you want to help with localization, there are two different cases: the locale already exists
+or not.
+
+If the locale does not exist you will need to create it by using the ``clock`` utility:
+
+.. code-block:: bash
+
+ ./clock locale create <your-locale>
+
+It will generate a directory in ``pendulum/locales`` named after your locale, with the following
+structure:
+
+.. code-block:: text
+
+ <your-locale>/
+ - custom.py
+ - locale.py
+
+The ``locale.py`` file must not be modified. It contains the translations provided by
+the CLDR database.
+
+The ``custom.py`` file is the one you want to modify. It contains the data needed
+by Pendulum that are not provided by the CLDR database. You can take the `en <https://github.com/sdispater/pendulum/tree/master/pendulum/locales/en/custom.py>`_
+data as a reference to see which data is needed.
+
+You should also add tests for the created or modified locale.
+
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..d65fb47
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,224 @@
+Pendulum
+########
+
+.. image:: https://img.shields.io/pypi/v/pendulum.svg
+ :target: https://pypi.python.org/pypi/pendulum
+
+.. image:: https://img.shields.io/pypi/l/pendulum.svg
+ :target: https://pypi.python.org/pypi/pendulum
+
+.. image:: https://img.shields.io/codecov/c/github/sdispater/pendulum/master.svg
+ :target: https://codecov.io/gh/sdispater/pendulum/branch/master
+
+.. image:: https://travis-ci.org/sdispater/pendulum.svg
+ :alt: Pendulum Build status
+ :target: https://travis-ci.org/sdispater/pendulum
+
+Python datetimes made easy.
+
+Supports Python **2.7** and **3.4+**.
+
+
+.. code-block:: python
+
+ >>> import pendulum
+
+ >>> now_in_paris = pendulum.now('Europe/Paris')
+ >>> now_in_paris
+ '2016-07-04T00:49:58.502116+02:00'
+
+ # Seamless timezone switching
+ >>> now_in_paris.in_timezone('UTC')
+ '2016-07-03T22:49:58.502116+00:00'
+
+ >>> tomorrow = pendulum.now().add(days=1)
+ >>> last_week = pendulum.now().subtract(weeks=1)
+
+ >>> past = pendulum.now().subtract(minutes=2)
+ >>> past.diff_for_humans()
+ >>> '2 minutes ago'
+
+ >>> delta = past - last_week
+ >>> delta.hours
+ 23
+ >>> delta.in_words(locale='en')
+ '6 days 23 hours 58 minutes'
+
+ # Proper handling of datetime normalization
+ >>> pendulum.datetime(2013, 3, 31, 2, 30, tz='Europe/Paris')
+ '2013-03-31T03:30:00+02:00' # 2:30 does not exist (Skipped time)
+
+ # Proper handling of dst transitions
+ >>> just_before = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, tz='Europe/Paris')
+ '2013-03-31T01:59:59.999999+01:00'
+ >>> just_before.add(microseconds=1)
+ '2013-03-31T03:00:00+02:00'
+
+
+Why Pendulum?
+=============
+
+Native ``datetime`` instances are enough for basic cases but when you face more complex use-cases
+they often show limitations and are not so intuitive to work with.
+``Pendulum`` provides a cleaner and more easy to use API while still relying on the standard library.
+So it's still ``datetime`` but better.
+
+Unlike other datetime libraries for Python, Pendulum is a drop-in replacement
+for the standard ``datetime`` class (it inherits from it), so, basically, you can replace all your ``datetime``
+instances by ``DateTime`` instances in you code (exceptions exist for libraries that check
+the type of the objects by using the ``type`` function like ``sqlite3`` or ``PyMySQL`` for instance).
+
+It also removes the notion of naive datetimes: each ``Pendulum`` instance is timezone-aware
+and by default in ``UTC`` for ease of use.
+
+Pendulum also improves the standard ``timedelta`` class by providing more intuitive methods and properties.
+
+
+Why not Arrow?
+==============
+
+Arrow is the most popular datetime library for Python right now, however its behavior
+and API can be erratic and unpredictable. The ``get()`` method can receive pretty much anything
+and it will try its best to return something while silently failing to handle some cases:
+
+.. code-block:: python
+
+ arrow.get('2016-1-17')
+ # <Arrow [2016-01-01T00:00:00+00:00]>
+
+ pendulum.parse('2016-1-17')
+ # <Pendulum [2016-01-17T00:00:00+00:00]>
+
+ arrow.get('20160413')
+ # <Arrow [1970-08-22T08:06:53+00:00]>
+
+ pendulum.parse('20160413')
+ # <Pendulum [2016-04-13T00:00:00+00:00]>
+
+ arrow.get('2016-W07-5')
+ # <Arrow [2016-01-01T00:00:00+00:00]>
+
+ pendulum.parse('2016-W07-5')
+ # <Pendulum [2016-02-19T00:00:00+00:00]>
+
+ # Working with DST
+ just_before = arrow.Arrow(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris')
+ just_after = just_before.replace(microseconds=1)
+ '2013-03-31T02:00:00+02:00'
+ # Should be 2013-03-31T03:00:00+02:00
+
+ (just_after.to('utc') - just_before.to('utc')).total_seconds()
+ -3599.999999
+ # Should be 1e-06
+
+ just_before = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris')
+ just_after = just_before.add(microseconds=1)
+ '2013-03-31T03:00:00+02:00'
+
+ (just_after.in_timezone('utc') - just_before.in_timezone('utc')).total_seconds()
+ 1e-06
+
+Those are a few examples showing that Arrow cannot always be trusted to have a consistent
+behavior with the data you are passing to it.
+
+
+Limitations
+===========
+
+Even though the ``DateTime`` class is a subclass of ``datetime`` there are some rare cases where
+it can't replace the native class directly. Here is a list (non-exhaustive) of the reported cases with
+a possible solution, if any:
+
+* ``sqlite3`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter:
+
+.. code-block:: python
+
+ from pendulum import DateTime
+ from sqlite3 import register_adapter
+
+ register_adapter(DateTime, lambda val: val.isoformat(' '))
+
+* ``mysqlclient`` (former ``MySQLdb``) and ``PyMySQL`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter:
+
+.. code-block:: python
+
+ import MySQLdb.converters
+ import pymysql.converters
+
+ from pendulum import DateTime
+
+ MySQLdb.converters.conversions[DateTime] = MySQLdb.converters.DateTime2literal
+ pymysql.converters.conversions[DateTime] = pymysql.converters.escape_datetime
+
+* ``django`` will use the ``isoformat()`` method to store datetimes in the database. However since ``pendulum`` is always timezone aware the offset information will always be returned by ``isoformat()`` raising an error, at least for MySQL databases. To work around it you can either create your own ``DateTimeField`` or use the previous workaround for ``MySQLdb``:
+
+.. code-block:: python
+
+ from django.db.models import DateTimeField as BaseDateTimeField
+ from pendulum import DateTime
+
+
+ class DateTimeField(BaseDateTimeField):
+
+ def value_to_string(self, obj):
+ val = self.value_from_object(obj)
+
+ if isinstance(value, DateTime):
+ return value.to_datetime_string()
+
+ return '' if val is None else val.isoformat()
+
+
+Resources
+=========
+
+* `Official Website <https://pendulum.eustace.io>`_
+* `Documentation <https://pendulum.eustace.io/docs/>`_
+* `Issue Tracker <https://github.com/sdispater/pendulum/issues>`_
+
+
+Contributing
+============
+
+Contributions are welcome, especially with localization.
+
+Getting started
+---------------
+
+To work on the Pendulum codebase, you'll want to clone the project locally
+and install the required depedendencies via `poetry <https://poetry.eustace.io>`_.
+
+.. code-block:: bash
+
+ $ git clone git@github.com:sdispater/pendulum.git
+ $ poetry install
+
+Localization
+------------
+
+If you want to help with localization, there are two different cases: the locale already exists
+or not.
+
+If the locale does not exist you will need to create it by using the ``clock`` utility:
+
+.. code-block:: bash
+
+ ./clock locale create <your-locale>
+
+It will generate a directory in ``pendulum/locales`` named after your locale, with the following
+structure:
+
+.. code-block:: text
+
+ <your-locale>/
+ - custom.py
+ - locale.py
+
+The ``locale.py`` file must not be modified. It contains the translations provided by
+the CLDR database.
+
+The ``custom.py`` file is the one you want to modify. It contains the data needed
+by Pendulum that are not provided by the CLDR database. You can take the `en <https://github.com/sdispater/pendulum/tree/master/pendulum/locales/en/custom.py>`_
+data as a reference to see which data is needed.
+
+You should also add tests for the created or modified locale.
diff --git a/build.py b/build.py
new file mode 100644
index 0000000..07d277e
--- /dev/null
+++ b/build.py
@@ -0,0 +1,87 @@
+import os
+import shutil
+import sys
+
+from distutils.command.build_ext import build_ext
+from distutils.core import Distribution
+from distutils.core import Extension
+from distutils.errors import CCompilerError
+from distutils.errors import DistutilsExecError
+from distutils.errors import DistutilsPlatformError
+
+
+# C Extensions
+with_extensions = os.getenv("PENDULUM_EXTENSIONS", None)
+
+if with_extensions == "1" or with_extensions is None:
+ with_extensions = True
+
+if with_extensions == "0" or hasattr(sys, "pypy_version_info"):
+ with_extensions = False
+
+extensions = []
+if with_extensions:
+ extensions = [
+ Extension("pendulum._extensions._helpers", ["pendulum/_extensions/_helpers.c"]),
+ Extension("pendulum.parsing._iso8601", ["pendulum/parsing/_iso8601.c"]),
+ ]
+
+
+class BuildFailed(Exception):
+
+ pass
+
+
+class ExtBuilder(build_ext):
+ # This class allows C extension building to fail.
+
+ built_extensions = []
+
+ def run(self):
+ try:
+ build_ext.run(self)
+ except (DistutilsPlatformError, FileNotFoundError):
+ print(
+ " Unable to build the C extensions, "
+ "Pendulum will use the pure python code instead."
+ )
+
+ def build_extension(self, ext):
+ try:
+ build_ext.build_extension(self, ext)
+ except (CCompilerError, DistutilsExecError, DistutilsPlatformError, ValueError):
+ print(
+ ' Unable to build the "{}" C extension, '
+ "Pendulum will use the pure python version of the extension.".format(
+ ext.name
+ )
+ )
+
+
+def build(setup_kwargs):
+ """
+ This function is mandatory in order to build the extensions.
+ """
+ distribution = Distribution({"name": "pendulum", "ext_modules": extensions})
+ distribution.package_dir = "pendulum"
+
+ cmd = ExtBuilder(distribution)
+ cmd.ensure_finalized()
+ cmd.run()
+
+ # Copy built extensions back to the project
+ for output in cmd.get_outputs():
+ relative_extension = os.path.relpath(output, cmd.build_lib)
+ if not os.path.exists(output):
+ continue
+
+ shutil.copyfile(output, relative_extension)
+ mode = os.stat(relative_extension).st_mode
+ mode |= (mode & 0o444) >> 2
+ os.chmod(relative_extension, mode)
+
+ return setup_kwargs
+
+
+if __name__ == "__main__":
+ build({})
diff --git a/pendulum/__init__.py b/pendulum/__init__.py
new file mode 100644
index 0000000..a85ed88
--- /dev/null
+++ b/pendulum/__init__.py
@@ -0,0 +1,315 @@
+from __future__ import absolute_import
+
+import datetime as _datetime
+
+from typing import Optional
+from typing import Union
+
+from .__version__ import __version__
+from .constants import DAYS_PER_WEEK
+from .constants import FRIDAY
+from .constants import HOURS_PER_DAY
+from .constants import MINUTES_PER_HOUR
+from .constants import MONDAY
+from .constants import MONTHS_PER_YEAR
+from .constants import SATURDAY
+from .constants import SECONDS_PER_DAY
+from .constants import SECONDS_PER_HOUR
+from .constants import SECONDS_PER_MINUTE
+from .constants import SUNDAY
+from .constants import THURSDAY
+from .constants import TUESDAY
+from .constants import WEDNESDAY
+from .constants import WEEKS_PER_YEAR
+from .constants import YEARS_PER_CENTURY
+from .constants import YEARS_PER_DECADE
+from .date import Date
+from .datetime import DateTime
+from .duration import Duration
+from .formatting import Formatter
+from .helpers import format_diff
+from .helpers import get_locale
+from .helpers import get_test_now
+from .helpers import has_test_now
+from .helpers import locale
+from .helpers import set_locale
+from .helpers import set_test_now
+from .helpers import test
+from .helpers import week_ends_at
+from .helpers import week_starts_at
+from .parser import parse
+from .period import Period
+from .time import Time
+from .tz import POST_TRANSITION
+from .tz import PRE_TRANSITION
+from .tz import TRANSITION_ERROR
+from .tz import UTC
+from .tz import local_timezone
+from .tz import set_local_timezone
+from .tz import test_local_timezone
+from .tz import timezone
+from .tz import timezones
+from .tz.timezone import Timezone as _Timezone
+from .utils._compat import _HAS_FOLD
+
+
+_TEST_NOW = None # type: Optional[DateTime]
+_LOCALE = "en"
+_WEEK_STARTS_AT = MONDAY
+_WEEK_ENDS_AT = SUNDAY
+
+_formatter = Formatter()
+
+
+def _safe_timezone(obj):
+ # type: (Optional[Union[str, float, _datetime.tzinfo, _Timezone]]) -> _Timezone
+ """
+ Creates a timezone instance
+ from a string, Timezone, TimezoneInfo or integer offset.
+ """
+ if isinstance(obj, _Timezone):
+ 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):
+ # pytz
+ if hasattr(obj, "localize"):
+ obj = obj.zone
+ elif obj.tzname(None) == "UTC":
+ return UTC
+ else:
+ offset = obj.utcoffset(None)
+
+ if offset is None:
+ offset = _datetime.timedelta(0)
+
+ obj = int(offset.total_seconds())
+
+ return timezone(obj)
+
+
+# Public API
+def datetime(
+ year, # type: int
+ month, # type: int
+ day, # type: int
+ hour=0, # type: int
+ minute=0, # type: int
+ second=0, # type: int
+ microsecond=0, # type: int
+ tz=UTC, # type: Optional[Union[str, float, _Timezone]]
+ dst_rule=POST_TRANSITION, # type: str
+): # type: (...) -> DateTime
+ """
+ Creates a new DateTime instance from a specific date and time.
+ """
+ if tz is not None:
+ tz = _safe_timezone(tz)
+
+ if not _HAS_FOLD:
+ dt = naive(year, month, day, hour, minute, second, microsecond)
+ else:
+ dt = _datetime.datetime(year, month, day, hour, minute, second, microsecond)
+ if tz is not None:
+ dt = tz.convert(dt, dst_rule=dst_rule)
+
+ return DateTime(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ tzinfo=dt.tzinfo,
+ fold=dt.fold,
+ )
+
+
+def local(
+ year, month, day, hour=0, minute=0, second=0, microsecond=0
+): # type: (int, int, int, int, int, int, int) -> DateTime
+ """
+ Return a DateTime in the local timezone.
+ """
+ return datetime(
+ year, month, day, hour, minute, second, microsecond, tz=local_timezone()
+ )
+
+
+def naive(
+ year, month, day, hour=0, minute=0, second=0, microsecond=0
+): # type: (int, int, int, int, int, int, int) -> DateTime
+ """
+ Return a naive DateTime.
+ """
+ return DateTime(year, month, day, hour, minute, second, microsecond)
+
+
+def date(year, month, day): # type: (int, int, int) -> Date
+ """
+ Create a new Date instance.
+ """
+ return Date(year, month, day)
+
+
+def time(hour, minute=0, second=0, microsecond=0): # type: (int, int, int, int) -> Time
+ """
+ Create a new Time instance.
+ """
+ return Time(hour, minute, second, microsecond)
+
+
+def instance(
+ dt, tz=UTC
+): # type: (_datetime.datetime, Optional[Union[str, _Timezone]]) -> DateTime
+ """
+ Create a DateTime instance from a datetime one.
+ """
+ if not isinstance(dt, _datetime.datetime):
+ raise ValueError("instance() only accepts datetime objects.")
+
+ if isinstance(dt, DateTime):
+ return dt
+
+ tz = dt.tzinfo or tz
+
+ # Checking for pytz/tzinfo
+ if isinstance(tz, _datetime.tzinfo) and not isinstance(tz, _Timezone):
+ # pytz
+ if hasattr(tz, "localize") and tz.zone:
+ tz = tz.zone
+ else:
+ # We have no sure way to figure out
+ # the timezone name, we fallback
+ # on a fixed offset
+ tz = tz.utcoffset(dt).total_seconds() / 3600
+
+ return datetime(
+ dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond, tz=tz
+ )
+
+
+def now(tz=None): # type: (Optional[Union[str, _Timezone]]) -> DateTime
+ """
+ Get a DateTime instance for the current date and time.
+ """
+ if has_test_now():
+ test_instance = get_test_now()
+ _tz = _safe_timezone(tz)
+
+ if tz is not None and _tz != test_instance.timezone:
+ test_instance = test_instance.in_tz(_tz)
+
+ return test_instance
+
+ 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 = _safe_timezone(tz)
+ dt = tz.convert(dt)
+
+ return DateTime(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ tzinfo=dt.tzinfo,
+ fold=dt.fold if _HAS_FOLD else 0,
+ )
+
+
+def today(tz="local"): # type: (Union[str, _Timezone]) -> DateTime
+ """
+ Create a DateTime instance for today.
+ """
+ return now(tz).start_of("day")
+
+
+def tomorrow(tz="local"): # type: (Union[str, _Timezone]) -> DateTime
+ """
+ Create a DateTime instance for today.
+ """
+ return today(tz).add(days=1)
+
+
+def yesterday(tz="local"): # type: (Union[str, _Timezone]) -> DateTime
+ """
+ Create a DateTime instance for today.
+ """
+ return today(tz).subtract(days=1)
+
+
+def from_format(
+ string, fmt, tz=UTC, locale=None, # noqa
+): # type: (str, str, Union[str, _Timezone], Optional[str]) -> DateTime
+ """
+ Creates a DateTime instance from a specific format.
+ """
+ parts = _formatter.parse(string, fmt, now(), locale=locale)
+ if parts["tz"] is None:
+ parts["tz"] = tz
+
+ return datetime(**parts)
+
+
+def from_timestamp(
+ timestamp, tz=UTC
+): # type: (Union[int, float], Union[str, _Timezone]) -> 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=0, # type: float
+ seconds=0, # type: float
+ microseconds=0, # type: float
+ milliseconds=0, # type: float
+ minutes=0, # type: float
+ hours=0, # type: float
+ weeks=0, # type: float
+ years=0, # type: float
+ months=0, # type: float
+): # type: (...) -> 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 period(start, end, absolute=False): # type: (DateTime, DateTime, bool) -> Period
+ """
+ Create a Period instance.
+ """
+ return Period(start, end, absolute=absolute)
diff --git a/pendulum/__version__.py b/pendulum/__version__.py
new file mode 100644
index 0000000..62b3483
--- /dev/null
+++ b/pendulum/__version__.py
@@ -0,0 +1 @@
+__version__ = "2.1.2"
diff --git a/pendulum/_extensions/__init__.py b/pendulum/_extensions/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/_extensions/__init__.py
diff --git a/pendulum/_extensions/_helpers.c b/pendulum/_extensions/_helpers.c
new file mode 100644
index 0000000..aa92ae5
--- /dev/null
+++ b/pendulum/_extensions/_helpers.c
@@ -0,0 +1,930 @@
+/* ------------------------------------------------------------------------- */
+
+#include <Python.h>
+#include <datetime.h>
+#include <structmember.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+/* ------------------------------------------------------------------------- */
+
+#define EPOCH_YEAR 1970
+
+#define DAYS_PER_N_YEAR 365
+#define DAYS_PER_L_YEAR 366
+
+#define USECS_PER_SEC 1000000
+
+#define SECS_PER_MIN 60
+#define SECS_PER_HOUR (60 * SECS_PER_MIN)
+#define SECS_PER_DAY (SECS_PER_HOUR * 24)
+
+// 400-year chunks always have 146097 days (20871 weeks).
+#define DAYS_PER_400_YEARS 146097L
+#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)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.
+const int64_t SECS_PER_100_YEARS[2] = {
+ (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY,
+ (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * 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.
+const int32_t SECS_PER_4_YEARS[2] = {
+ (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.
+const int32_t SECS_PER_YEAR[2] = {
+ DAYS_PER_N_YEAR * SECS_PER_DAY,
+ DAYS_PER_L_YEAR *SECS_PER_DAY};
+
+#define MONTHS_PER_YEAR 12
+
+// The month lengths in non-leap and leap years respectively.
+const int32_t DAYS_PER_MONTHS[2][13] = {
+ {-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.
+const int32_t MONTHS_OFFSETS[2][14] = {
+ {-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}};
+
+const int DAY_OF_WEEK_TABLE[12] = {
+ 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
+
+#define TM_SUNDAY 0
+#define TM_MONDAY 1
+#define TM_TUESDAY 2
+#define TM_WEDNESDAY 3
+#define TM_THURSDAY 4
+#define TM_FRIDAY 5
+#define TM_SATURDAY 6
+
+#define TM_JANUARY 0
+#define TM_FEBRUARY 1
+#define TM_MARCH 2
+#define TM_APRIL 3
+#define TM_MAY 4
+#define TM_JUNE 5
+#define TM_JULY 6
+#define TM_AUGUST 7
+#define TM_SEPTEMBER 8
+#define TM_OCTOBER 9
+#define TM_NOVEMBER 10
+#define TM_DECEMBER 11
+
+/* ------------------------------------------------------------------------- */
+
+int _p(int y)
+{
+ return y + y / 4 - y / 100 + y / 400;
+}
+
+int _is_leap(int year)
+{
+ return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
+}
+
+int _is_long_year(int year)
+{
+ return (_p(year) % 7 == 4) || (_p(year - 1) % 7 == 3);
+}
+
+int _week_day(int year, int month, int day)
+{
+ int y;
+ int w;
+
+ y = year - (month < 3);
+
+ w = (_p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7;
+
+ if (!w)
+ {
+ w = 7;
+ }
+
+ return w;
+}
+
+int _days_in_year(int year)
+{
+ if (_is_leap(year))
+ {
+ return DAYS_PER_L_YEAR;
+ }
+
+ return DAYS_PER_N_YEAR;
+}
+
+int _day_number(int year, int month, int day)
+{
+ month = (month + 9) % 12;
+ year = year - month / 10;
+
+ return (
+ 365 * year + year / 4 - year / 100 + year / 400 + (month * 306 + 5) / 10 + (day - 1));
+}
+
+int _get_offset(PyObject *dt)
+{
+ PyObject *tzinfo;
+ PyObject *offset;
+
+ tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo;
+
+ if (tzinfo != Py_None)
+ {
+ offset = PyObject_CallMethod(tzinfo, "utcoffset", "O", dt);
+
+ return PyDateTime_DELTA_GET_DAYS(offset) * SECS_PER_DAY + PyDateTime_DELTA_GET_SECONDS(offset);
+ }
+
+ return 0;
+}
+
+int _has_tzinfo(PyObject *dt)
+{
+ return ((_PyDateTime_BaseTZInfo *)(dt))->hastzinfo;
+}
+
+char *_get_tz_name(PyObject *dt)
+{
+ PyObject *tzinfo;
+ char *tz = "";
+
+ tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo;
+
+ if (tzinfo != Py_None)
+ {
+ if (PyObject_HasAttrString(tzinfo, "name"))
+ {
+ // Pendulum timezone
+ tz = (char *)PyUnicode_AsUTF8(
+ PyObject_GetAttrString(tzinfo, "name"));
+ }
+ else if (PyObject_HasAttrString(tzinfo, "zone"))
+ {
+ // pytz timezone
+ tz = (char *)PyUnicode_AsUTF8(
+ PyObject_GetAttrString(tzinfo, "zone"));
+ }
+ }
+
+ return tz;
+}
+
+/* ------------------------ Custom Types ------------------------------- */
+
+/*
+ * class Diff():
+ */
+typedef struct
+{
+ PyObject_HEAD int years;
+ int months;
+ int days;
+ int hours;
+ int minutes;
+ int seconds;
+ int microseconds;
+ int total_days;
+} Diff;
+
+/*
+ * def __init__(self, years, months, days, hours, minutes, seconds, microseconds, total_days):
+ * self.years = years
+ * # ...
+*/
+static int Diff_init(Diff *self, PyObject *args, PyObject *kwargs)
+{
+ int years;
+ int months;
+ int days;
+ int hours;
+ int minutes;
+ int seconds;
+ int microseconds;
+ int total_days;
+
+ if (!PyArg_ParseTuple(args, "iiiiiii", &years, &months, &days, &hours, &minutes, &seconds, &microseconds, &total_days))
+ return -1;
+
+ self->years = years;
+ self->months = months;
+ self->days = days;
+ self->hours = hours;
+ self->minutes = minutes;
+ self->seconds = seconds;
+ self->microseconds = microseconds;
+ self->total_days = total_days;
+
+ return 0;
+}
+
+/*
+ * def __repr__(self):
+ * return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format(
+ * self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds
+ * )
+ */
+static PyObject *Diff_repr(Diff *self)
+{
+ char repr[82] = {0};
+
+ sprintf(
+ repr,
+ "%d years %d months %d days %d hours %d minutes %d seconds %d microseconds",
+ self->years,
+ self->months,
+ self->days,
+ self->hours,
+ self->minutes,
+ self->seconds,
+ self->microseconds);
+
+ return PyUnicode_FromString(repr);
+}
+
+/*
+ * Instantiate new Diff_type object
+ * Skip overhead of calling PyObject_New and PyObject_Init.
+ * Directly allocate object.
+ */
+static PyObject *new_diff_ex(int years, int months, int days, int hours, int minutes, int seconds, int microseconds, int total_days, PyTypeObject *type)
+{
+ Diff *self = (Diff *)(type->tp_alloc(type, 0));
+
+ if (self != NULL)
+ {
+ self->years = years;
+ self->months = months;
+ self->days = days;
+ self->hours = hours;
+ self->minutes = minutes;
+ self->seconds = seconds;
+ self->microseconds = microseconds;
+ self->total_days = total_days;
+ }
+
+ return (PyObject *)self;
+}
+
+/*
+ * Class member / class attributes
+ */
+static PyMemberDef Diff_members[] = {
+ {"years", T_INT, offsetof(Diff, years), 0, "years in diff"},
+ {"months", T_INT, offsetof(Diff, months), 0, "months in diff"},
+ {"days", T_INT, offsetof(Diff, days), 0, "days in diff"},
+ {"hours", T_INT, offsetof(Diff, hours), 0, "hours in diff"},
+ {"minutes", T_INT, offsetof(Diff, minutes), 0, "minutes in diff"},
+ {"seconds", T_INT, offsetof(Diff, seconds), 0, "seconds in diff"},
+ {"microseconds", T_INT, offsetof(Diff, microseconds), 0, "microseconds in diff"},
+ {"total_days", T_INT, offsetof(Diff, total_days), 0, "total days in diff"},
+ {NULL}};
+
+static PyTypeObject Diff_type = {
+ PyVarObject_HEAD_INIT(NULL, 0) "PreciseDiff", /* tp_name */
+ sizeof(Diff), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ 0, /* tp_dealloc */
+ 0, /* tp_print */
+ 0, /* tp_getattr */
+ 0, /* tp_setattr */
+ 0, /* tp_as_async */
+ (reprfunc)Diff_repr, /* tp_repr */
+ 0, /* tp_as_number */
+ 0, /* tp_as_sequence */
+ 0, /* tp_as_mapping */
+ 0, /* tp_hash */
+ 0, /* tp_call */
+ (reprfunc)Diff_repr, /* tp_str */
+ 0, /* tp_getattro */
+ 0, /* tp_setattro */
+ 0, /* tp_as_buffer */
+ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
+ "Precise difference between two datetime objects", /* tp_doc */
+};
+
+#define new_diff(years, months, days, hours, minutes, seconds, microseconds, total_days) new_diff_ex(years, months, days, hours, minutes, seconds, microseconds, total_days, &Diff_type)
+
+/* -------------------------- Functions --------------------------*/
+
+PyObject *is_leap(PyObject *self, PyObject *args)
+{
+ PyObject *leap;
+ int year;
+
+ if (!PyArg_ParseTuple(args, "i", &year))
+ {
+ PyErr_SetString(
+ PyExc_ValueError, "Invalid parameters");
+ return NULL;
+ }
+
+ leap = PyBool_FromLong(_is_leap(year));
+
+ return leap;
+}
+
+PyObject *is_long_year(PyObject *self, PyObject *args)
+{
+ PyObject *is_long;
+ int year;
+
+ if (!PyArg_ParseTuple(args, "i", &year))
+ {
+ PyErr_SetString(
+ PyExc_ValueError, "Invalid parameters");
+ return NULL;
+ }
+
+ is_long = PyBool_FromLong(_is_long_year(year));
+
+ return is_long;
+}
+
+PyObject *week_day(PyObject *self, PyObject *args)
+{
+ PyObject *wd;
+ int year;
+ int month;
+ int day;
+
+ if (!PyArg_ParseTuple(args, "iii", &year, &month, &day))
+ {
+ PyErr_SetString(
+ PyExc_ValueError, "Invalid parameters");
+ return NULL;
+ }
+
+ wd = PyLong_FromLong(_week_day(year, month, day));
+
+ return wd;
+}
+
+PyObject *days_in_year(PyObject *self, PyObject *args)
+{
+ PyObject *ndays;
+ int year;
+
+ if (!PyArg_ParseTuple(args, "i", &year))
+ {
+ PyErr_SetString(
+ PyExc_ValueError, "Invalid parameters");
+ return NULL;
+ }
+
+ ndays = PyLong_FromLong(_days_in_year(year));
+
+ return ndays;
+}
+
+PyObject *timestamp(PyObject *self, PyObject *args)
+{
+ int64_t result;
+ PyObject *dt;
+
+ if (!PyArg_ParseTuple(args, "O", &dt))
+ {
+ PyErr_SetString(
+ PyExc_ValueError, "Invalid parameters");
+ return NULL;
+ }
+
+ int year = (double)PyDateTime_GET_YEAR(dt);
+ int month = PyDateTime_GET_MONTH(dt);
+ int day = PyDateTime_GET_DAY(dt);
+ int hour = PyDateTime_DATE_GET_HOUR(dt);
+ int minute = PyDateTime_DATE_GET_MINUTE(dt);
+ int second = PyDateTime_DATE_GET_SECOND(dt);
+
+ result = (year - 1970) * 365 + MONTHS_OFFSETS[0][month];
+ result += (int)floor((double)(year - 1968) / 4);
+ result -= (year - 1900) / 100;
+ result += (year - 1600) / 400;
+
+ if (_is_leap(year) && month < 3)
+ {
+ result -= 1;
+ }
+
+ result += day - 1;
+ result *= 24;
+ result += hour;
+ result *= 60;
+ result += minute;
+ result *= 60;
+ result += second;
+
+ return PyLong_FromSsize_t(result);
+}
+
+PyObject *local_time(PyObject *self, PyObject *args)
+{
+ double unix_time;
+ int32_t utc_offset;
+ int32_t year;
+ int32_t microsecond;
+ int64_t seconds;
+ int32_t leap_year;
+ int64_t sec_per_100years;
+ int64_t sec_per_4years;
+ int32_t sec_per_year;
+ int32_t month;
+ int32_t day;
+ int32_t month_offset;
+ int32_t hour;
+ int32_t minute;
+ int32_t second;
+
+ if (!PyArg_ParseTuple(args, "dii", &unix_time, &utc_offset, &microsecond))
+ {
+ PyErr_SetString(
+ PyExc_ValueError, "Invalid parameters");
+ return NULL;
+ }
+
+ year = EPOCH_YEAR;
+ seconds = (int64_t)floor(unix_time);
+
+ // Shift to a base year that is 400-year aligned.
+ if (seconds >= 0)
+ {
+ seconds -= 10957L * SECS_PER_DAY;
+ year += 30; // == 2000;
+ }
+ else
+ {
+ seconds += (int64_t)(146097L - 10957L) * 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 and seconds
+ hour = seconds / SECS_PER_HOUR;
+ seconds %= SECS_PER_HOUR;
+ minute = seconds / SECS_PER_MIN;
+ second = seconds % SECS_PER_MIN;
+
+ return Py_BuildValue("NNNNNNN",
+ PyLong_FromLong(year),
+ PyLong_FromLong(month),
+ PyLong_FromLong(day),
+ PyLong_FromLong(hour),
+ PyLong_FromLong(minute),
+ PyLong_FromLong(second),
+ PyLong_FromLong(microsecond));
+}
+
+// Calculate a precise difference between two datetimes.
+PyObject *precise_diff(PyObject *self, PyObject *args)
+{
+ PyObject *dt1;
+ PyObject *dt2;
+
+ if (!PyArg_ParseTuple(args, "OO", &dt1, &dt2))
+ {
+ PyErr_SetString(
+ PyExc_ValueError, "Invalid parameters");
+ return NULL;
+ }
+
+ int year_diff = 0;
+ int month_diff = 0;
+ int day_diff = 0;
+ int hour_diff = 0;
+ int minute_diff = 0;
+ int second_diff = 0;
+ int microsecond_diff = 0;
+ int sign = 1;
+ int year;
+ int month;
+ int leap;
+ int days_in_last_month;
+ int days_in_month;
+ int dt1_year = PyDateTime_GET_YEAR(dt1);
+ int dt2_year = PyDateTime_GET_YEAR(dt2);
+ int dt1_month = PyDateTime_GET_MONTH(dt1);
+ int dt2_month = PyDateTime_GET_MONTH(dt2);
+ int dt1_day = PyDateTime_GET_DAY(dt1);
+ int dt2_day = PyDateTime_GET_DAY(dt2);
+ int dt1_hour = 0;
+ int dt2_hour = 0;
+ int dt1_minute = 0;
+ int dt2_minute = 0;
+ int dt1_second = 0;
+ int dt2_second = 0;
+ int dt1_microsecond = 0;
+ int dt2_microsecond = 0;
+ int dt1_total_seconds = 0;
+ int dt2_total_seconds = 0;
+ int dt1_offset = 0;
+ int dt2_offset = 0;
+ int dt1_is_datetime = PyDateTime_Check(dt1);
+ int dt2_is_datetime = PyDateTime_Check(dt2);
+ char *tz1 = "";
+ char *tz2 = "";
+ int in_same_tz = 0;
+ int total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day));
+
+ // If both dates are datetimes, we check
+ // If we are in the same timezone
+ if (dt1_is_datetime && dt2_is_datetime)
+ {
+ if (_has_tzinfo(dt1))
+ {
+ tz1 = _get_tz_name(dt1);
+ dt1_offset = _get_offset(dt1);
+ }
+
+ if (_has_tzinfo(dt2))
+ {
+ tz2 = _get_tz_name(dt2);
+ dt2_offset = _get_offset(dt2);
+ }
+
+ in_same_tz = tz1 == tz2 && strncmp(tz1, "", 1);
+ }
+
+ // If we have datetimes (and not only dates)
+ // we get the information we need
+ if (dt1_is_datetime)
+ {
+ dt1_hour = PyDateTime_DATE_GET_HOUR(dt1);
+ dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1);
+ dt1_second = PyDateTime_DATE_GET_SECOND(dt1);
+ dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1);
+
+ if ((!in_same_tz && dt1_offset != 0) || total_days == 0)
+ {
+ dt1_hour -= dt1_offset / SECS_PER_HOUR;
+ dt1_offset %= SECS_PER_HOUR;
+ dt1_minute -= dt1_offset / SECS_PER_MIN;
+ dt1_offset %= SECS_PER_MIN;
+ dt1_second -= dt1_offset;
+
+ if (dt1_second < 0)
+ {
+ dt1_second += 60;
+ dt1_minute -= 1;
+ }
+ else if (dt1_second > 60)
+ {
+ dt1_second -= 60;
+ dt1_minute += 1;
+ }
+
+ if (dt1_minute < 0)
+ {
+ dt1_minute += 60;
+ dt1_hour -= 1;
+ }
+ else if (dt1_minute > 60)
+ {
+ dt1_minute -= 60;
+ dt1_hour += 1;
+ }
+
+ if (dt1_hour < 0)
+ {
+ dt1_hour += 24;
+ dt1_day -= 1;
+ }
+ else if (dt1_hour > 24)
+ {
+ dt1_hour -= 24;
+ dt1_day += 1;
+ }
+ }
+
+ dt1_total_seconds = (dt1_hour * SECS_PER_HOUR + dt1_minute * SECS_PER_MIN + dt1_second);
+ }
+
+ if (dt2_is_datetime)
+ {
+ dt2_hour = PyDateTime_DATE_GET_HOUR(dt2);
+ dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2);
+ dt2_second = PyDateTime_DATE_GET_SECOND(dt2);
+ dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2);
+
+ if ((!in_same_tz && dt2_offset != 0) || total_days == 0)
+ {
+ dt2_hour -= dt2_offset / SECS_PER_HOUR;
+ dt2_offset %= SECS_PER_HOUR;
+ dt2_minute -= dt2_offset / SECS_PER_MIN;
+ dt2_offset %= SECS_PER_MIN;
+ dt2_second -= dt2_offset;
+
+ if (dt2_second < 0)
+ {
+ dt2_second += 60;
+ dt2_minute -= 1;
+ }
+ else if (dt2_second > 60)
+ {
+ dt2_second -= 60;
+ dt2_minute += 1;
+ }
+
+ if (dt2_minute < 0)
+ {
+ dt2_minute += 60;
+ dt2_hour -= 1;
+ }
+ else if (dt2_minute > 60)
+ {
+ dt2_minute -= 60;
+ dt2_hour += 1;
+ }
+
+ if (dt2_hour < 0)
+ {
+ dt2_hour += 24;
+ dt2_day -= 1;
+ }
+ else if (dt2_hour > 24)
+ {
+ dt2_hour -= 24;
+ dt2_day += 1;
+ }
+ }
+
+ dt2_total_seconds = (dt2_hour * SECS_PER_HOUR + dt2_minute * SECS_PER_MIN + dt2_second);
+ }
+
+ // Direct comparison between two datetimes does not work
+ // so we need to check by properties
+ int dt1_gt_dt2 = (dt1_year > dt2_year || (dt1_year == dt2_year && dt1_month > dt2_month) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day > dt2_day) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds > dt2_total_seconds) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds == dt2_total_seconds && dt1_microsecond > dt2_microsecond));
+
+ if (dt1_gt_dt2)
+ {
+ PyObject *temp;
+ temp = dt1;
+ dt1 = dt2;
+ dt2 = temp;
+ sign = -1;
+
+ // Retrieving properties
+ dt1_year = PyDateTime_GET_YEAR(dt1);
+ dt2_year = PyDateTime_GET_YEAR(dt2);
+ dt1_month = PyDateTime_GET_MONTH(dt1);
+ dt2_month = PyDateTime_GET_MONTH(dt2);
+ dt1_day = PyDateTime_GET_DAY(dt1);
+ dt2_day = PyDateTime_GET_DAY(dt2);
+
+ if (dt2_is_datetime)
+ {
+ dt1_hour = PyDateTime_DATE_GET_HOUR(dt1);
+ dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1);
+ dt1_second = PyDateTime_DATE_GET_SECOND(dt1);
+ dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1);
+ }
+
+ if (dt1_is_datetime)
+ {
+ dt2_hour = PyDateTime_DATE_GET_HOUR(dt2);
+ dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2);
+ dt2_second = PyDateTime_DATE_GET_SECOND(dt2);
+ dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2);
+ }
+
+ total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day));
+ }
+
+ year_diff = dt2_year - dt1_year;
+ month_diff = dt2_month - dt1_month;
+ day_diff = dt2_day - dt1_day;
+ hour_diff = dt2_hour - dt1_hour;
+ minute_diff = dt2_minute - dt1_minute;
+ second_diff = dt2_second - dt1_second;
+ microsecond_diff = dt2_microsecond - dt1_microsecond;
+
+ if (microsecond_diff < 0)
+ {
+ microsecond_diff += 1e6;
+ second_diff -= 1;
+ }
+
+ if (second_diff < 0)
+ {
+ second_diff += 60;
+ minute_diff -= 1;
+ }
+
+ if (minute_diff < 0)
+ {
+ minute_diff += 60;
+ hour_diff -= 1;
+ }
+
+ if (hour_diff < 0)
+ {
+ hour_diff += 24;
+ day_diff -= 1;
+ }
+
+ if (day_diff < 0)
+ {
+ // If we have a difference in days,
+ // we have to check if they represent months
+ year = dt2_year;
+ month = dt2_month;
+
+ if (month == 1)
+ {
+ month = 12;
+ year -= 1;
+ }
+ else
+ {
+ month -= 1;
+ }
+
+ leap = _is_leap(year);
+
+ days_in_last_month = DAYS_PER_MONTHS[leap][month];
+ days_in_month = DAYS_PER_MONTHS[_is_leap(dt2_year)][dt2_month];
+
+ if (day_diff < days_in_month - days_in_last_month)
+ {
+ // We don't have a full month, we calculate days
+ if (days_in_last_month < dt1_day)
+ {
+ day_diff += dt1_day;
+ }
+ else
+ {
+ day_diff += days_in_last_month;
+ }
+ }
+ else if (day_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
+ day_diff = 0;
+ month_diff += 1;
+ }
+ else
+ {
+ // We have a full month
+ day_diff += days_in_last_month;
+ }
+
+ month_diff -= 1;
+ }
+
+ if (month_diff < 0)
+ {
+ month_diff += 12;
+ year_diff -= 1;
+ }
+
+ return new_diff(
+ year_diff * sign,
+ month_diff * sign,
+ day_diff * sign,
+ hour_diff * sign,
+ minute_diff * sign,
+ second_diff * sign,
+ microsecond_diff * sign,
+ total_days * sign);
+}
+
+/* ------------------------------------------------------------------------- */
+
+static PyMethodDef helpers_methods[] = {
+ {"is_leap",
+ (PyCFunction)is_leap,
+ METH_VARARGS,
+ PyDoc_STR("Checks if a year is a leap year.")},
+ {"is_long_year",
+ (PyCFunction)is_long_year,
+ METH_VARARGS,
+ PyDoc_STR("Checks if a year is a long year.")},
+ {"week_day",
+ (PyCFunction)week_day,
+ METH_VARARGS,
+ PyDoc_STR("Returns the weekday number.")},
+ {"days_in_year",
+ (PyCFunction)days_in_year,
+ METH_VARARGS,
+ PyDoc_STR("Returns the number of days in the given year.")},
+ {"timestamp",
+ (PyCFunction)timestamp,
+ METH_VARARGS,
+ PyDoc_STR("Returns the timestamp of the given datetime.")},
+ {"local_time",
+ (PyCFunction)local_time,
+ METH_VARARGS,
+ PyDoc_STR("Returns a UNIX time as a broken down time for a particular transition type.")},
+ {"precise_diff",
+ (PyCFunction)precise_diff,
+ METH_VARARGS,
+ PyDoc_STR("Calculate a precise difference between two datetimes.")},
+ {NULL}};
+
+/* ------------------------------------------------------------------------- */
+
+static struct PyModuleDef moduledef = {
+ PyModuleDef_HEAD_INIT,
+ "_helpers",
+ NULL,
+ -1,
+ helpers_methods,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+};
+
+PyMODINIT_FUNC
+PyInit__helpers(void)
+{
+ PyObject *module;
+
+ PyDateTime_IMPORT;
+
+ module = PyModule_Create(&moduledef);
+
+ if (module == NULL)
+ return NULL;
+
+ // Diff declaration
+ Diff_type.tp_new = PyType_GenericNew;
+ Diff_type.tp_members = Diff_members;
+ Diff_type.tp_init = (initproc)Diff_init;
+
+ if (PyType_Ready(&Diff_type) < 0)
+ return NULL;
+
+ PyModule_AddObject(module, "PreciseDiff", (PyObject *)&Diff_type);
+
+ return module;
+}
diff --git a/pendulum/_extensions/helpers.py b/pendulum/_extensions/helpers.py
new file mode 100644
index 0000000..16d078c
--- /dev/null
+++ b/pendulum/_extensions/helpers.py
@@ -0,0 +1,358 @@
+import datetime
+import math
+import typing
+
+from collections import namedtuple
+
+from ..constants import DAY_OF_WEEK_TABLE
+from ..constants import DAYS_PER_L_YEAR
+from ..constants import DAYS_PER_MONTHS
+from ..constants import DAYS_PER_N_YEAR
+from ..constants import EPOCH_YEAR
+from ..constants import MONTHS_OFFSETS
+from ..constants import SECS_PER_4_YEARS
+from ..constants import SECS_PER_100_YEARS
+from ..constants import SECS_PER_400_YEARS
+from ..constants import SECS_PER_DAY
+from ..constants import SECS_PER_HOUR
+from ..constants import SECS_PER_MIN
+from ..constants import SECS_PER_YEAR
+from ..constants import TM_DECEMBER
+from ..constants import TM_JANUARY
+
+
+class PreciseDiff(
+ namedtuple(
+ "PreciseDiff",
+ "years months days " "hours minutes seconds microseconds " "total_days",
+ )
+):
+ def __repr__(self):
+ return (
+ "{years} years "
+ "{months} months "
+ "{days} days "
+ "{hours} hours "
+ "{minutes} minutes "
+ "{seconds} seconds "
+ "{microseconds} microseconds"
+ ).format(
+ years=self.years,
+ months=self.months,
+ days=self.days,
+ hours=self.hours,
+ minutes=self.minutes,
+ seconds=self.seconds,
+ microseconds=self.microseconds,
+ )
+
+
+def is_leap(year): # type: (int) -> bool
+ return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
+
+
+def is_long_year(year): # type: (int) -> bool
+ def p(y):
+ return y + y // 4 - y // 100 + y // 400
+
+ return p(year) % 7 == 4 or p(year - 1) % 7 == 3
+
+
+def week_day(year, month, day): # type: (int, int, 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): # type: (int) -> int
+ if is_leap(year):
+ return DAYS_PER_L_YEAR
+
+ return DAYS_PER_N_YEAR
+
+
+def timestamp(dt): # type: (datetime.datetime) -> int
+ year = dt.year
+
+ result = (year - 1970) * 365 + MONTHS_OFFSETS[0][dt.month]
+ result += (year - 1968) // 4
+ result -= (year - 1900) // 100
+ result += (year - 1600) // 400
+
+ if is_leap(year) and dt.month < 3:
+ result -= 1
+
+ result += dt.day - 1
+ result *= 24
+ result += dt.hour
+ result *= 60
+ result += dt.minute
+ result *= 60
+ result += dt.second
+
+ return result
+
+
+def local_time(
+ unix_time, utc_offset, microseconds
+): # type: (int, int, int) -> typing.Tuple[int, int, int, int, int, int, int]
+ """
+ Returns a UNIX time as a broken down time
+ for a particular transition type.
+
+ :type unix_time: int
+ :type utc_offset: int
+ :type microseconds: int
+
+ :rtype: tuple
+ """
+ year = EPOCH_YEAR
+ seconds = int(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 // SECS_PER_HOUR
+ seconds %= SECS_PER_HOUR
+ minute = seconds // SECS_PER_MIN
+ second = seconds % SECS_PER_MIN
+
+ return (year, month, day, hour, minute, second, microseconds)
+
+
+def precise_diff(
+ d1, d2
+): # type: (typing.Union[datetime.datetime, datetime.date], typing.Union[datetime.datetime, datetime.date]) -> PreciseDiff
+ """
+ Calculate a precise difference between two datetimes.
+
+ :param d1: The first datetime
+ :type d1: datetime.datetime or datetime.date
+
+ :param d2: The second datetime
+ :type d2: datetime.datetime or datetime.date
+
+ :rtype: PreciseDiff
+ """
+ sign = 1
+
+ if d1 == d2:
+ return PreciseDiff(0, 0, 0, 0, 0, 0, 0, 0)
+
+ tzinfo1 = d1.tzinfo if isinstance(d1, datetime.datetime) else None
+ tzinfo2 = 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:
+ if hasattr(tzinfo1, "name"):
+ # Pendulum timezone
+ tz1 = tzinfo1.name
+ elif hasattr(tzinfo1, "zone"):
+ # pytz timezone
+ tz1 = tzinfo1.zone
+
+ if hasattr(tzinfo2, "name"):
+ tz2 = tzinfo2.name
+ elif hasattr(tzinfo2, "zone"):
+ tz2 = tzinfo2.zone
+
+ 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, month, day): # type: (int, int, 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)
+ )
diff --git a/pendulum/constants.py b/pendulum/constants.py
new file mode 100644
index 0000000..3712df3
--- /dev/null
+++ b/pendulum/constants.py
@@ -0,0 +1,107 @@
+# The day constants
+SUNDAY = 0
+MONDAY = 1
+TUESDAY = 2
+WEDNESDAY = 3
+THURSDAY = 4
+FRIDAY = 5
+SATURDAY = 6
+
+# 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/pendulum/date.py b/pendulum/date.py
new file mode 100644
index 0000000..41a9883
--- /dev/null
+++ b/pendulum/date.py
@@ -0,0 +1,891 @@
+from __future__ import absolute_import
+from __future__ import division
+
+import calendar
+import math
+
+from datetime import date
+from datetime import timedelta
+
+import pendulum
+
+from .constants import FRIDAY
+from .constants import MONDAY
+from .constants import MONTHS_PER_YEAR
+from .constants import SATURDAY
+from .constants import SUNDAY
+from .constants import THURSDAY
+from .constants import TUESDAY
+from .constants import WEDNESDAY
+from .constants import YEARS_PER_CENTURY
+from .constants import YEARS_PER_DECADE
+from .exceptions import PendulumException
+from .helpers import add_duration
+from .mixins.default import FormattableMixin
+from .period import Period
+
+
+class Date(FormattableMixin, date):
+
+ # Names of days of the week
+ _days = {
+ SUNDAY: "Sunday",
+ MONDAY: "Monday",
+ TUESDAY: "Tuesday",
+ WEDNESDAY: "Wednesday",
+ THURSDAY: "Thursday",
+ FRIDAY: "Friday",
+ SATURDAY: "Saturday",
+ }
+
+ _MODIFIERS_VALID_UNITS = ["day", "week", "month", "year", "decade", "century"]
+
+ # Getters/Setters
+
+ def set(self, year=None, month=None, day=None):
+ return self.replace(year=year, month=month, day=day)
+
+ @property
+ def day_of_week(self):
+ """
+ Returns the day of the week (0-6).
+
+ :rtype: int
+ """
+ return self.isoweekday() % 7
+
+ @property
+ def day_of_year(self):
+ """
+ Returns the day of the year (1-366).
+
+ :rtype: int
+ """
+ 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):
+ return self.isocalendar()[1]
+
+ @property
+ def days_in_month(self):
+ return calendar.monthrange(self.year, self.month)[1]
+
+ @property
+ def week_of_month(self):
+ first_day_of_month = self.replace(day=1)
+
+ return self.week_of_year - first_day_of_month.week_of_year + 1
+
+ @property
+ def age(self):
+ return self.diff(abs=False).in_years()
+
+ @property
+ def quarter(self):
+ return int(math.ceil(self.month / 3))
+
+ # String Formatting
+
+ def to_date_string(self):
+ """
+ Format the instance as date.
+
+ :rtype: str
+ """
+ return self.strftime("%Y-%m-%d")
+
+ def to_formatted_date_string(self):
+ """
+ Format the instance as a readable date.
+
+ :rtype: str
+ """
+ return self.strftime("%b %d, %Y")
+
+ def __repr__(self):
+ return (
+ "{klass}("
+ "{year}, {month}, {day}"
+ ")".format(
+ klass=self.__class__.__name__,
+ year=self.year,
+ month=self.month,
+ day=self.day,
+ )
+ )
+
+ # COMPARISONS
+
+ def closest(self, dt1, dt2):
+ """
+ Get the closest date from the instance.
+
+ :type dt1: Date or date
+ :type dt2: Date or date
+
+ :rtype: Date
+ """
+ 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, dt2):
+ """
+ Get the farthest date from the instance.
+
+ :type dt1: Date or date
+ :type dt2: Date or date
+
+ :rtype: Date
+ """
+ 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):
+ """
+ Determines if the instance is in the future, ie. greater than now.
+
+ :rtype: bool
+ """
+ return self > self.today()
+
+ def is_past(self):
+ """
+ Determines if the instance is in the past, ie. less than now.
+
+ :rtype: bool
+ """
+ return self < self.today()
+
+ def is_leap_year(self):
+ """
+ Determines if the instance is a leap year.
+
+ :rtype: bool
+ """
+ return calendar.isleap(self.year)
+
+ def is_long_year(self):
+ """
+ Determines if the instance is a long year
+
+ See link `<https://en.wikipedia.org/wiki/ISO_8601#Week_dates>`_
+
+ :rtype: bool
+ """
+ return Date(self.year, 12, 28).isocalendar()[1] == 53
+
+ def is_same_day(self, dt):
+ """
+ Checks if the passed in date is the same day as the instance current day.
+
+ :type dt: Date or date
+
+ :rtype: bool
+ """
+ return self == dt
+
+ def is_anniversary(self, dt=None):
+ """
+ Check if its the anniversary.
+
+ Compares the date/month values of the two dates.
+
+ :rtype: bool
+ """
+ if dt is None:
+ dt = Date.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 SUBSTRACTIONS
+
+ def add(self, years=0, months=0, weeks=0, days=0):
+ """
+ Add duration to the instance.
+
+ :param years: The number of years
+ :type years: int
+
+ :param months: The number of months
+ :type months: int
+
+ :param weeks: The number of weeks
+ :type weeks: int
+
+ :param days: The number of days
+ :type days: int
+
+ :rtype: Date
+ """
+ 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=0, months=0, weeks=0, days=0):
+ """
+ Remove duration from the instance.
+
+ :param years: The number of years
+ :type years: int
+
+ :param months: The number of months
+ :type months: int
+
+ :param weeks: The number of weeks
+ :type weeks: int
+
+ :param days: The number of days
+ :type days: int
+
+ :rtype: Date
+ """
+ return self.add(years=-years, months=-months, weeks=-weeks, days=-days)
+
+ def _add_timedelta(self, delta):
+ """
+ Add timedelta duration to the instance.
+
+ :param delta: The timedelta instance
+ :type delta: pendulum.Duration or datetime.timedelta
+
+ :rtype: Date
+ """
+ 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):
+ """
+ Remove timedelta duration from the instance.
+
+ :param delta: The timedelta instance
+ :type delta: pendulum.Duration or datetime.timedelta
+
+ :rtype: Date
+ """
+ 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):
+ if not isinstance(other, timedelta):
+ return NotImplemented
+
+ return self._add_timedelta(other)
+
+ def __sub__(self, other):
+ 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=None, abs=True):
+ """
+ Returns the difference between two Date objects as a Period.
+
+ :type dt: Date or None
+
+ :param abs: Whether to return an absolute interval or not
+ :type abs: bool
+
+ :rtype: Period
+ """
+ if dt is None:
+ dt = self.today()
+
+ return Period(self, Date(dt.year, dt.month, dt.day), absolute=abs)
+
+ def diff_for_humans(self, other=None, absolute=False, locale=None):
+ """
+ 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
+
+ :type other: Date
+
+ :param absolute: removes time difference modifiers ago, after, etc
+ :type absolute: bool
+
+ :param locale: The locale to use for localization
+ :type locale: str
+
+ :rtype: str
+ """
+ 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):
+ """
+ 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
+ :type unit: str
+
+ :rtype: Date
+ """
+ if unit not in self._MODIFIERS_VALID_UNITS:
+ raise ValueError('Invalid unit "{}" for start_of()'.format(unit))
+
+ return getattr(self, "_start_of_{}".format(unit))()
+
+ def end_of(self, unit):
+ """
+ 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
+ :type unit: str
+
+ :rtype: Date
+ """
+ if unit not in self._MODIFIERS_VALID_UNITS:
+ raise ValueError('Invalid unit "%s" for end_of()' % unit)
+
+ return getattr(self, "_end_of_%s" % unit)()
+
+ def _start_of_day(self):
+ """
+ Compatibility method.
+
+ :rtype: Date
+ """
+ return self
+
+ def _end_of_day(self):
+ """
+ Compatibility method
+
+ :rtype: Date
+ """
+ return self
+
+ def _start_of_month(self):
+ """
+ Reset the date to the first day of the month.
+
+ :rtype: Date
+ """
+ return self.set(self.year, self.month, 1)
+
+ def _end_of_month(self):
+ """
+ Reset the date to the last day of the month.
+
+ :rtype: Date
+ """
+ return self.set(self.year, self.month, self.days_in_month)
+
+ def _start_of_year(self):
+ """
+ Reset the date to the first day of the year.
+
+ :rtype: Date
+ """
+ return self.set(self.year, 1, 1)
+
+ def _end_of_year(self):
+ """
+ Reset the date to the last day of the year.
+
+ :rtype: Date
+ """
+ return self.set(self.year, 12, 31)
+
+ def _start_of_decade(self):
+ """
+ Reset the date to the first day of the decade.
+
+ :rtype: Date
+ """
+ year = self.year - self.year % YEARS_PER_DECADE
+
+ return self.set(year, 1, 1)
+
+ def _end_of_decade(self):
+ """
+ Reset the date to the last day of the decade.
+
+ :rtype: Date
+ """
+ year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1
+
+ return self.set(year, 12, 31)
+
+ def _start_of_century(self):
+ """
+ Reset the date to the first day of the century.
+
+ :rtype: Date
+ """
+ year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1
+
+ return self.set(year, 1, 1)
+
+ def _end_of_century(self):
+ """
+ Reset the date to the last day of the century.
+
+ :rtype: Date
+ """
+ year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY
+
+ return self.set(year, 12, 31)
+
+ def _start_of_week(self):
+ """
+ Reset the date to the first day of the week.
+
+ :rtype: Date
+ """
+ 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):
+ """
+ Reset the date to the last day of the week.
+
+ :rtype: Date
+ """
+ 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=None):
+ """
+ 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.
+ :type day_of_week: int or None
+
+ :rtype: Date
+ """
+ if day_of_week is None:
+ day_of_week = self.day_of_week
+
+ if day_of_week < SUNDAY or day_of_week > SATURDAY:
+ 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=None):
+ """
+ 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.
+ :type day_of_week: int or None
+
+ :rtype: Date
+ """
+ if day_of_week is None:
+ day_of_week = self.day_of_week
+
+ if day_of_week < SUNDAY or day_of_week > SATURDAY:
+ 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, day_of_week=None):
+ """
+ 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
+ :type unit: str
+
+ :type day_of_week: int or None
+
+ :rtype: Date
+ """
+ if unit not in ["month", "quarter", "year"]:
+ raise ValueError('Invalid unit "{}" for first_of()'.format(unit))
+
+ return getattr(self, "_first_of_{}".format(unit))(day_of_week)
+
+ def last_of(self, unit, day_of_week=None):
+ """
+ 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
+ :type unit: str
+
+ :type day_of_week: int or None
+
+ :rtype: Date
+ """
+ if unit not in ["month", "quarter", "year"]:
+ raise ValueError('Invalid unit "{}" for first_of()'.format(unit))
+
+ return getattr(self, "_last_of_{}".format(unit))(day_of_week)
+
+ def nth_of(self, unit, nth, day_of_week):
+ """
+ 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
+ :type unit: str
+
+ :type nth: int
+
+ :type day_of_week: int or None
+
+ :rtype: Date
+ """
+ if unit not in ["month", "quarter", "year"]:
+ raise ValueError('Invalid unit "{}" for first_of()'.format(unit))
+
+ dt = getattr(self, "_nth_of_{}".format(unit))(nth, day_of_week)
+ if dt is False:
+ raise PendulumException(
+ "Unable to find occurence {} of {} in {}".format(
+ nth, self._days[day_of_week], unit
+ )
+ )
+
+ return dt
+
+ def _first_of_month(self, day_of_week):
+ """
+ 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.
+
+ :type day_of_week: int
+
+ :rtype: Date
+ """
+ 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 - 1) % 7
+
+ 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=None):
+ """
+ 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.
+
+ :type day_of_week: int or None
+
+ :rtype: Date
+ """
+ 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 - 1) % 7
+
+ 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, day_of_week):
+ """
+ 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.
+
+ :type nth: int
+
+ :type day_of_week: int or None
+
+ :rtype: Date
+ """
+ if nth == 1:
+ return self.first_of("month", day_of_week)
+
+ dt = self.first_of("month")
+ check = dt.format("YYYY-MM")
+ for i 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 False
+
+ def _first_of_quarter(self, day_of_week=None):
+ """
+ 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.
+
+ :type day_of_week: int or None
+
+ :rtype: Date
+ """
+ return self.set(self.year, self.quarter * 3 - 2, 1).first_of(
+ "month", day_of_week
+ )
+
+ def _last_of_quarter(self, day_of_week=None):
+ """
+ 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.
+
+ :type day_of_week: int or None
+
+ :rtype: Date
+ """
+ return self.set(self.year, self.quarter * 3, 1).last_of("month", day_of_week)
+
+ def _nth_of_quarter(self, nth, day_of_week):
+ """
+ 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.
+
+ :type nth: int
+
+ :type day_of_week: int or None
+
+ :rtype: Date
+ """
+ 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 i 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 False
+
+ return self.set(self.year, dt.month, dt.day)
+
+ def _first_of_year(self, day_of_week=None):
+ """
+ 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.
+
+ :type day_of_week: int or None
+
+ :rtype: Date
+ """
+ return self.set(month=1).first_of("month", day_of_week)
+
+ def _last_of_year(self, day_of_week=None):
+ """
+ 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.
+
+ :type day_of_week: int or None
+
+ :rtype: Date
+ """
+ return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week)
+
+ def _nth_of_year(self, nth, day_of_week):
+ """
+ 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.
+
+ :type nth: int
+
+ :type day_of_week: int or None
+
+ :rtype: Date
+ """
+ if nth == 1:
+ return self.first_of("year", day_of_week)
+
+ dt = self.first_of("year")
+ year = dt.year
+ for i 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 False
+
+ return self.set(self.year, dt.month, dt.day)
+
+ def average(self, dt=None):
+ """
+ Modify the current instance to the average
+ of a given instance (default now) and the current instance.
+
+ :type dt: Date or date
+
+ :rtype: Date
+ """
+ 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):
+ return pendulum.today().date()
+
+ @classmethod
+ def fromtimestamp(cls, t):
+ dt = super(Date, cls).fromtimestamp(t)
+
+ return cls(dt.year, dt.month, dt.day)
+
+ @classmethod
+ def fromordinal(cls, n):
+ dt = super(Date, cls).fromordinal(n)
+
+ return cls(dt.year, dt.month, dt.day)
+
+ def replace(self, year=None, month=None, day=None):
+ 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/pendulum/datetime.py b/pendulum/datetime.py
new file mode 100644
index 0000000..6f1d5cf
--- /dev/null
+++ b/pendulum/datetime.py
@@ -0,0 +1,1563 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+from __future__ import division
+
+import calendar
+import datetime
+
+from typing import Optional
+from typing import TypeVar
+from typing import Union
+
+import pendulum
+
+from .constants import ATOM
+from .constants import COOKIE
+from .constants import MINUTES_PER_HOUR
+from .constants import MONTHS_PER_YEAR
+from .constants import RFC822
+from .constants import RFC850
+from .constants import RFC1036
+from .constants import RFC1123
+from .constants import RFC2822
+from .constants import RSS
+from .constants import SATURDAY
+from .constants import SECONDS_PER_DAY
+from .constants import SECONDS_PER_MINUTE
+from .constants import SUNDAY
+from .constants import W3C
+from .constants import YEARS_PER_CENTURY
+from .constants import YEARS_PER_DECADE
+from .date import Date
+from .exceptions import PendulumException
+from .helpers import add_duration
+from .helpers import timestamp
+from .period import Period
+from .time import Time
+from .tz import UTC
+from .tz.timezone import Timezone
+from .utils._compat import _HAS_FOLD
+
+
+_D = TypeVar("_D", bound="DateTime")
+
+
+class DateTime(datetime.datetime, Date):
+
+ EPOCH = None # type: DateTime
+
+ # Formats
+
+ _FORMATS = {
+ "atom": ATOM,
+ "cookie": COOKIE,
+ "iso8601": lambda dt: dt.isoformat(),
+ "rfc822": RFC822,
+ "rfc850": RFC850,
+ "rfc1036": RFC1036,
+ "rfc1123": RFC1123,
+ "rfc2822": RFC2822,
+ "rfc3339": lambda dt: dt.isoformat(),
+ "rss": RSS,
+ "w3c": W3C,
+ }
+
+ _EPOCH = datetime.datetime(1970, 1, 1, tzinfo=UTC)
+
+ _MODIFIERS_VALID_UNITS = [
+ "second",
+ "minute",
+ "hour",
+ "day",
+ "week",
+ "month",
+ "year",
+ "decade",
+ "century",
+ ]
+
+ if not _HAS_FOLD:
+
+ def __new__(
+ cls,
+ year,
+ month,
+ day,
+ hour=0,
+ minute=0,
+ second=0,
+ microsecond=0,
+ tzinfo=None,
+ fold=0,
+ ):
+ self = datetime.datetime.__new__(
+ cls, year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo
+ )
+
+ self._fold = fold
+
+ return self
+
+ @classmethod
+ def now(cls, tz=None): # type: (Optional[Union[str, Timezone]]) -> DateTime
+ """
+ Get a DateTime instance for the current date and time.
+ """
+ return pendulum.now(tz)
+
+ @classmethod
+ def utcnow(cls): # type: () -> DateTime
+ """
+ Get a DateTime instance for the current date and time in UTC.
+ """
+ return pendulum.now(UTC)
+
+ @classmethod
+ def today(cls): # type: () -> DateTime
+ return pendulum.now()
+
+ @classmethod
+ def strptime(cls, time, fmt): # type: (str, str) -> DateTime
+ return pendulum.instance(datetime.datetime.strptime(time, fmt))
+
+ # Getters/Setters
+
+ def set(
+ self,
+ year=None,
+ month=None,
+ day=None,
+ hour=None,
+ minute=None,
+ second=None,
+ microsecond=None,
+ tz=None,
+ ):
+ 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 pendulum.datetime(
+ year, month, day, hour, minute, second, microsecond, tz=tz
+ )
+
+ if not _HAS_FOLD:
+
+ @property
+ def fold(self):
+ return self._fold
+
+ def timestamp(self):
+ if self.tzinfo is None:
+ s = timestamp(self)
+
+ return s + self.microsecond / 1e6
+ else:
+ kwargs = {"tzinfo": self.tzinfo}
+
+ if _HAS_FOLD:
+ kwargs["fold"] = self.fold
+
+ dt = datetime.datetime(
+ self.year,
+ self.month,
+ self.day,
+ self.hour,
+ self.minute,
+ self.second,
+ self.microsecond,
+ **kwargs
+ )
+ return (dt - self._EPOCH).total_seconds()
+
+ @property
+ def float_timestamp(self):
+ return self.timestamp()
+
+ @property
+ def int_timestamp(self):
+ # Workaround needed to avoid inaccuracy
+ # for far into the future datetimes
+ kwargs = {"tzinfo": self.tzinfo}
+
+ if _HAS_FOLD:
+ kwargs["fold"] = self.fold
+
+ dt = datetime.datetime(
+ self.year,
+ self.month,
+ self.day,
+ self.hour,
+ self.minute,
+ self.second,
+ self.microsecond,
+ **kwargs
+ )
+
+ delta = dt - self._EPOCH
+
+ return delta.days * SECONDS_PER_DAY + delta.seconds
+
+ @property
+ def offset(self):
+ return self.get_offset()
+
+ @property
+ def offset_hours(self):
+ return self.get_offset() / SECONDS_PER_MINUTE / MINUTES_PER_HOUR
+
+ @property
+ def timezone(self): # type: () -> Optional[Timezone]
+ if not isinstance(self.tzinfo, Timezone):
+ return
+
+ return self.tzinfo
+
+ @property
+ def tz(self): # type: () -> Optional[Timezone]
+ return self.timezone
+
+ @property
+ def timezone_name(self): # type: () -> Optional[str]
+ tz = self.timezone
+
+ if tz is None:
+ return None
+
+ return tz.name
+
+ @property
+ def age(self):
+ return self.date().diff(self.now(self.tz).date(), abs=False).in_years()
+
+ def is_local(self):
+ return self.offset == self.in_timezone(pendulum.local_timezone()).offset
+
+ def is_utc(self):
+ return self.offset == UTC.offset
+
+ def is_dst(self):
+ return self.dst() != datetime.timedelta()
+
+ def get_offset(self):
+ return int(self.utcoffset().total_seconds())
+
+ def date(self):
+ return Date(self.year, self.month, self.day)
+
+ def time(self):
+ return Time(self.hour, self.minute, self.second, self.microsecond)
+
+ def naive(self): # type: (_D) -> _D
+ """
+ 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, month, day):
+ """
+ Returns a new instance with the current date set to a different date.
+
+ :param year: The year
+ :type year: int
+
+ :param month: The month
+ :type month: int
+
+ :param day: The day
+ :type day: int
+
+ :rtype: DateTime
+ """
+ return self.set(year=int(year), month=int(month), day=int(day))
+
+ def at(self, hour, minute=0, second=0, microsecond=0):
+ """
+ Returns a new instance with the current time to a different time.
+
+ :param hour: The hour
+ :type hour: int
+
+ :param minute: The minute
+ :type minute: int
+
+ :param second: The second
+ :type second: int
+
+ :param microsecond: The microsecond
+ :type microsecond: int
+
+ :rtype: DateTime
+ """
+ return self.set(
+ hour=hour, minute=minute, second=second, microsecond=microsecond
+ )
+
+ def in_timezone(self, tz): # type: (Union[str, Timezone]) -> DateTime
+ """
+ Set the instance's timezone from a string or object.
+ """
+ tz = pendulum._safe_timezone(tz)
+
+ return tz.convert(self, dst_rule=pendulum.POST_TRANSITION)
+
+ def in_tz(self, tz): # type: (Union[str, Timezone]) -> DateTime
+ """
+ Set the instance's timezone from a string or object.
+ """
+ return self.in_timezone(tz)
+
+ # STRING FORMATTING
+
+ def to_time_string(self):
+ """
+ Format the instance as time.
+
+ :rtype: str
+ """
+ return self.format("HH:mm:ss")
+
+ def to_datetime_string(self):
+ """
+ Format the instance as date and time.
+
+ :rtype: str
+ """
+ return self.format("YYYY-MM-DD HH:mm:ss")
+
+ def to_day_datetime_string(self):
+ """
+ Format the instance as day, date and time (in english).
+
+ :rtype: str
+ """
+ return self.format("ddd, MMM D, YYYY h:mm A", locale="en")
+
+ def to_atom_string(self):
+ """
+ Format the instance as ATOM.
+
+ :rtype: str
+ """
+ return self._to_string("atom")
+
+ def to_cookie_string(self):
+ """
+ Format the instance as COOKIE.
+
+ :rtype: str
+ """
+ return self._to_string("cookie", locale="en")
+
+ def to_iso8601_string(self):
+ """
+ Format the instance as ISO 8601.
+
+ :rtype: str
+ """
+ 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):
+ """
+ Format the instance as RFC 822.
+
+ :rtype: str
+ """
+ return self._to_string("rfc822")
+
+ def to_rfc850_string(self):
+ """
+ Format the instance as RFC 850.
+
+ :rtype: str
+ """
+ return self._to_string("rfc850")
+
+ def to_rfc1036_string(self):
+ """
+ Format the instance as RFC 1036.
+
+ :rtype: str
+ """
+ return self._to_string("rfc1036")
+
+ def to_rfc1123_string(self):
+ """
+ Format the instance as RFC 1123.
+
+ :rtype: str
+ """
+ return self._to_string("rfc1123")
+
+ def to_rfc2822_string(self):
+ """
+ Format the instance as RFC 2822.
+
+ :rtype: str
+ """
+ return self._to_string("rfc2822")
+
+ def to_rfc3339_string(self):
+ """
+ Format the instance as RFC 3339.
+
+ :rtype: str
+ """
+ return self._to_string("rfc3339")
+
+ def to_rss_string(self):
+ """
+ Format the instance as RSS.
+
+ :rtype: str
+ """
+ return self._to_string("rss")
+
+ def to_w3c_string(self):
+ """
+ Format the instance as W3C.
+
+ :rtype: str
+ """
+ return self._to_string("w3c")
+
+ def _to_string(self, fmt, locale=None):
+ """
+ Format the instance to a common string format.
+
+ :param fmt: The name of the string format
+ :type fmt: string
+
+ :param locale: The locale to use
+ :type locale: str or None
+
+ :rtype: str
+ """
+ if fmt not in self._FORMATS:
+ raise ValueError("Format [{}] is not supported".format(fmt))
+
+ fmt = self._FORMATS[fmt]
+ if callable(fmt):
+ return fmt(self)
+
+ return self.format(fmt, locale=locale)
+
+ def __str__(self):
+ return self.isoformat("T")
+
+ def __repr__(self):
+ us = ""
+ if self.microsecond:
+ us = ", {}".format(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=self.tzinfo,
+ )
+
+ # Comparisons
+ def closest(self, dt1, dt2, *dts):
+ """
+ Get the farthest date from the instance.
+
+ :type dt1: datetime.datetime
+ :type dt2: datetime.datetime
+ :type dts: list[datetime.datetime,]
+
+ :rtype: DateTime
+ """
+ dt1 = pendulum.instance(dt1)
+ dt2 = pendulum.instance(dt2)
+ dts = [dt1, dt2] + [pendulum.instance(x) for x in dts]
+ dts = [(abs(self - dt), dt) for dt in dts]
+
+ return min(dts)[1]
+
+ def farthest(self, dt1, dt2, *dts):
+ """
+ Get the farthest date from the instance.
+
+ :type dt1: datetime.datetime
+ :type dt2: datetime.datetime
+ :type dts: list[datetime.datetime,]
+
+ :rtype: DateTime
+ """
+ dt1 = pendulum.instance(dt1)
+ dt2 = pendulum.instance(dt2)
+
+ dts = [dt1, dt2] + [pendulum.instance(x) for x in dts]
+ dts = [(abs(self - dt), dt) for dt in dts]
+
+ return max(dts)[1]
+
+ def is_future(self):
+ """
+ Determines if the instance is in the future, ie. greater than now.
+
+ :rtype: bool
+ """
+ return self > self.now(self.timezone)
+
+ def is_past(self):
+ """
+ Determines if the instance is in the past, ie. less than now.
+
+ :rtype: bool
+ """
+ return self < self.now(self.timezone)
+
+ def is_long_year(self):
+ """
+ Determines if the instance is a long year
+
+ See link `https://en.wikipedia.org/wiki/ISO_8601#Week_dates`_
+
+ :rtype: bool
+ """
+ return (
+ pendulum.datetime(self.year, 12, 28, 0, 0, 0, tz=self.tz).isocalendar()[1]
+ == 53
+ )
+
+ def is_same_day(self, dt):
+ """
+ Checks if the passed in date is the same day
+ as the instance current day.
+
+ :type dt: DateTime or datetime or str or int
+
+ :rtype: bool
+ """
+ dt = pendulum.instance(dt)
+
+ return self.to_date_string() == dt.to_date_string()
+
+ def is_anniversary(self, dt=None):
+ """
+ Check if its the anniversary.
+ Compares the date/month values of the two dates.
+
+ :rtype: bool
+ """
+ if dt is None:
+ dt = self.now(self.tz)
+
+ instance = pendulum.instance(dt)
+
+ 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 SUBSTRACTIONS
+
+ def add(
+ self,
+ years=0,
+ months=0,
+ weeks=0,
+ days=0,
+ hours=0,
+ minutes=0,
+ seconds=0,
+ microseconds=0,
+ ): # type: (_D, int, int, int, int, int, int, int, int) -> _D
+ """
+ Add a duration to the instance.
+
+ If we're adding units of variable length (i.e., years, months),
+ move forward from curren 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.tzinfo is None:
+ return pendulum.datetime(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ tz=self.tz,
+ )
+
+ dt = self.__class__(
+ 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=0,
+ months=0,
+ weeks=0,
+ days=0,
+ hours=0,
+ minutes=0,
+ seconds=0,
+ microseconds=0,
+ ):
+ """
+ Remove duration from the instance.
+
+ :param years: The number of years
+ :type years: int
+
+ :param months: The number of months
+ :type months: int
+
+ :param weeks: The number of weeks
+ :type weeks: int
+
+ :param days: The number of days
+ :type days: int
+
+ :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: DateTime
+ """
+ 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):
+ """
+ Add timedelta duration to the instance.
+
+ :param delta: The timedelta instance
+ :type delta: pendulum.Duration or datetime.timedelta
+
+ :rtype: DateTime
+ """
+ if isinstance(delta, pendulum.Period):
+ 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(
+ years=delta.years, months=delta.months, seconds=delta._total
+ )
+
+ return self.add(seconds=delta.total_seconds())
+
+ def _subtract_timedelta(self, delta):
+ """
+ Remove timedelta duration from the instance.
+
+ :param delta: The timedelta instance
+ :type delta: pendulum.Duration or datetime.timedelta
+
+ :rtype: DateTime
+ """
+ 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(self, dt=None, abs=True):
+ """
+ Returns the difference between two DateTime objects represented as a Duration.
+
+ :type dt: DateTime or None
+
+ :param abs: Whether to return an absolute interval or not
+ :type abs: bool
+
+ :rtype: Period
+ """
+ if dt is None:
+ dt = self.now(self.tz)
+
+ return Period(self, dt, absolute=abs)
+
+ def diff_for_humans(
+ self,
+ other=None, # type: Optional[DateTime]
+ absolute=False, # type: bool
+ locale=None, # type: Optional[str]
+ ): # type: (...) -> 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):
+ """
+ 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
+
+ :param unit: The unit to reset to
+ :type unit: str
+
+ :rtype: DateTime
+ """
+ if unit not in self._MODIFIERS_VALID_UNITS:
+ raise ValueError('Invalid unit "{}" for start_of()'.format(unit))
+
+ return getattr(self, "_start_of_{}".format(unit))()
+
+ def end_of(self, unit):
+ """
+ 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
+
+ :param unit: The unit to reset to
+ :type unit: str
+
+ :rtype: DateTime
+ """
+ if unit not in self._MODIFIERS_VALID_UNITS:
+ raise ValueError('Invalid unit "%s" for end_of()' % unit)
+
+ return getattr(self, "_end_of_%s" % unit)()
+
+ def _start_of_second(self):
+ """
+ Reset microseconds to 0.
+
+ :rtype: DateTime
+ """
+ return self.set(microsecond=0)
+
+ def _end_of_second(self):
+ """
+ Set microseconds to 999999.
+
+ :rtype: DateTime
+ """
+ return self.set(microsecond=999999)
+
+ def _start_of_minute(self):
+ """
+ Reset seconds and microseconds to 0.
+
+ :rtype: DateTime
+ """
+ return self.set(second=0, microsecond=0)
+
+ def _end_of_minute(self):
+ """
+ Set seconds to 59 and microseconds to 999999.
+
+ :rtype: DateTime
+ """
+ return self.set(second=59, microsecond=999999)
+
+ def _start_of_hour(self):
+ """
+ Reset minutes, seconds and microseconds to 0.
+
+ :rtype: DateTime
+ """
+ return self.set(minute=0, second=0, microsecond=0)
+
+ def _end_of_hour(self):
+ """
+ Set minutes and seconds to 59 and microseconds to 999999.
+
+ :rtype: DateTime
+ """
+ return self.set(minute=59, second=59, microsecond=999999)
+
+ def _start_of_day(self):
+ """
+ Reset the time to 00:00:00
+
+ :rtype: DateTime
+ """
+ return self.at(0, 0, 0, 0)
+
+ def _end_of_day(self):
+ """
+ Reset the time to 23:59:59.999999
+
+ :rtype: DateTime
+ """
+ return self.at(23, 59, 59, 999999)
+
+ def _start_of_month(self):
+ """
+ Reset the date to the first day of the month and the time to 00:00:00.
+
+ :rtype: DateTime
+ """
+ return self.set(self.year, self.month, 1, 0, 0, 0, 0)
+
+ def _end_of_month(self):
+ """
+ Reset the date to the last day of the month
+ and the time to 23:59:59.999999.
+
+ :rtype: DateTime
+ """
+ return self.set(self.year, self.month, self.days_in_month, 23, 59, 59, 999999)
+
+ def _start_of_year(self):
+ """
+ Reset the date to the first day of the year and the time to 00:00:00.
+
+ :rtype: DateTime
+ """
+ return self.set(self.year, 1, 1, 0, 0, 0, 0)
+
+ def _end_of_year(self):
+ """
+ Reset the date to the last day of the year
+ and the time to 23:59:59.999999
+
+ :rtype: DateTime
+ """
+ return self.set(self.year, 12, 31, 23, 59, 59, 999999)
+
+ def _start_of_decade(self):
+ """
+ Reset the date to the first day of the decade
+ and the time to 00:00:00.
+
+ :rtype: DateTime
+ """
+ year = self.year - self.year % YEARS_PER_DECADE
+ return self.set(year, 1, 1, 0, 0, 0, 0)
+
+ def _end_of_decade(self):
+ """
+ Reset the date to the last day of the decade
+ and the time to 23:59:59.999999.
+
+ :rtype: DateTime
+ """
+ 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):
+ """
+ Reset the date to the first day of the century
+ and the time to 00:00:00.
+
+ :rtype: DateTime
+ """
+ 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):
+ """
+ Reset the date to the last day of the century
+ and the time to 23:59:59.999999.
+
+ :rtype: DateTime
+ """
+ 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):
+ """
+ Reset the date to the first day of the week
+ and the time to 00:00:00.
+
+ :rtype: DateTime
+ """
+ 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):
+ """
+ Reset the date to the last day of the week
+ and the time to 23:59:59.
+
+ :rtype: DateTime
+ """
+ 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=None, keep_time=False):
+ """
+ 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.
+
+ :param day_of_week: The next day of week to reset to.
+ :type day_of_week: int or None
+
+ :param keep_time: Whether to keep the time information or not.
+ :type keep_time: bool
+
+ :rtype: DateTime
+ """
+ if day_of_week is None:
+ day_of_week = self.day_of_week
+
+ if day_of_week < SUNDAY or day_of_week > SATURDAY:
+ raise ValueError("Invalid day of week")
+
+ if keep_time:
+ dt = self
+ else:
+ dt = 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=None, keep_time=False):
+ """
+ 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.
+
+ :param day_of_week: The previous day of week to reset to.
+ :type day_of_week: int or None
+
+ :param keep_time: Whether to keep the time information or not.
+ :type keep_time: bool
+
+ :rtype: DateTime
+ """
+ if day_of_week is None:
+ day_of_week = self.day_of_week
+
+ if day_of_week < SUNDAY or day_of_week > SATURDAY:
+ raise ValueError("Invalid day of week")
+
+ if keep_time:
+ dt = self
+ else:
+ dt = 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, day_of_week=None):
+ """
+ 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.
+
+ :param unit: The unit to use
+ :type unit: str
+
+ :type day_of_week: int or None
+
+ :rtype: DateTime
+ """
+ if unit not in ["month", "quarter", "year"]:
+ raise ValueError('Invalid unit "{}" for first_of()'.format(unit))
+
+ return getattr(self, "_first_of_{}".format(unit))(day_of_week)
+
+ def last_of(self, unit, day_of_week=None):
+ """
+ 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.
+
+ :param unit: The unit to use
+ :type unit: str
+
+ :type day_of_week: int or None
+
+ :rtype: DateTime
+ """
+ if unit not in ["month", "quarter", "year"]:
+ raise ValueError('Invalid unit "{}" for first_of()'.format(unit))
+
+ return getattr(self, "_last_of_{}".format(unit))(day_of_week)
+
+ def nth_of(self, unit, nth, day_of_week):
+ """
+ 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.
+
+ :param unit: The unit to use
+ :type unit: str
+
+ :type nth: int
+
+ :type day_of_week: int or None
+
+ :rtype: DateTime
+ """
+ if unit not in ["month", "quarter", "year"]:
+ raise ValueError('Invalid unit "{}" for first_of()'.format(unit))
+
+ dt = getattr(self, "_nth_of_{}".format(unit))(nth, day_of_week)
+ if dt is False:
+ raise PendulumException(
+ "Unable to find occurence {} of {} in {}".format(
+ nth, self._days[day_of_week], unit
+ )
+ )
+
+ return dt
+
+ def _first_of_month(self, day_of_week):
+ """
+ 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.
+
+ :type day_of_week: int
+
+ :rtype: DateTime
+ """
+ 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 - 1) % 7
+
+ 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=None):
+ """
+ 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.
+
+ :type day_of_week: int or None
+
+ :rtype: DateTime
+ """
+ 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 - 1) % 7
+
+ 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, day_of_week):
+ """
+ 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.
+
+ :type nth: int
+
+ :type day_of_week: int or None
+
+ :rtype: DateTime
+ """
+ if nth == 1:
+ return self.first_of("month", day_of_week)
+
+ dt = self.first_of("month")
+ check = dt.format("%Y-%M")
+ for i 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 False
+
+ def _first_of_quarter(self, day_of_week=None):
+ """
+ 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.
+
+ :type day_of_week: int or None
+
+ :rtype: DateTime
+ """
+ return self.on(self.year, self.quarter * 3 - 2, 1).first_of(
+ "month", day_of_week
+ )
+
+ def _last_of_quarter(self, day_of_week=None):
+ """
+ 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.
+
+ :type day_of_week: int or None
+
+ :rtype: DateTime
+ """
+ return self.on(self.year, self.quarter * 3, 1).last_of("month", day_of_week)
+
+ def _nth_of_quarter(self, nth, day_of_week):
+ """
+ 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.
+
+ :type nth: int
+
+ :type day_of_week: int or None
+
+ :rtype: DateTime
+ """
+ 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 i 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 False
+
+ return self.on(self.year, dt.month, dt.day).start_of("day")
+
+ def _first_of_year(self, day_of_week=None):
+ """
+ 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.
+
+ :type day_of_week: int or None
+
+ :rtype: DateTime
+ """
+ return self.set(month=1).first_of("month", day_of_week)
+
+ def _last_of_year(self, day_of_week=None):
+ """
+ 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.
+
+ :type day_of_week: int or None
+
+ :rtype: DateTime
+ """
+ return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week)
+
+ def _nth_of_year(self, nth, day_of_week):
+ """
+ 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.
+
+ :type nth: int
+
+ :type day_of_week: int or None
+
+ :rtype: DateTime
+ """
+ if nth == 1:
+ return self.first_of("year", day_of_week)
+
+ dt = self.first_of("year")
+ year = dt.year
+ for i 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 False
+
+ return self.on(self.year, dt.month, dt.day).start_of("day")
+
+ def average(self, dt=None):
+ """
+ Modify the current instance to the average
+ of a given instance (default now) and the current instance.
+
+ :type dt: DateTime or datetime
+
+ :rtype: DateTime
+ """
+ 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
+ )
+
+ def __sub__(self, other):
+ 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 = pendulum.instance(other)
+
+ return other.diff(self, False)
+
+ def __rsub__(self, 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 = pendulum.instance(other)
+
+ return self.diff(other, False)
+
+ def __add__(self, other):
+ if not isinstance(other, datetime.timedelta):
+ return NotImplemented
+
+ return self._add_timedelta_(other)
+
+ def __radd__(self, other):
+ return self.__add__(other)
+
+ # Native methods override
+
+ @classmethod
+ def fromtimestamp(cls, t, tz=None):
+ return pendulum.instance(datetime.datetime.fromtimestamp(t, tz=tz), tz=tz)
+
+ @classmethod
+ def utcfromtimestamp(cls, t):
+ return pendulum.instance(datetime.datetime.utcfromtimestamp(t), tz=None)
+
+ @classmethod
+ def fromordinal(cls, n):
+ return pendulum.instance(datetime.datetime.fromordinal(n), tz=None)
+
+ @classmethod
+ def combine(cls, date, time):
+ return pendulum.instance(datetime.datetime.combine(date, time), tz=None)
+
+ def astimezone(self, tz=None):
+ return pendulum.instance(super(DateTime, self).astimezone(tz))
+
+ def replace(
+ self,
+ year=None,
+ month=None,
+ day=None,
+ hour=None,
+ minute=None,
+ second=None,
+ microsecond=None,
+ tzinfo=True,
+ fold=None,
+ ):
+ 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
+
+ transition_rule = pendulum.POST_TRANSITION
+ if fold is not None:
+ transition_rule = pendulum.PRE_TRANSITION
+ if fold:
+ transition_rule = pendulum.POST_TRANSITION
+
+ return pendulum.datetime(
+ year,
+ month,
+ day,
+ hour,
+ minute,
+ second,
+ microsecond,
+ tz=tzinfo,
+ dst_rule=transition_rule,
+ )
+
+ def __getnewargs__(self):
+ return (self,)
+
+ def _getstate(self, protocol=3):
+ return (
+ self.year,
+ self.month,
+ self.day,
+ self.hour,
+ self.minute,
+ self.second,
+ self.microsecond,
+ self.tzinfo,
+ )
+
+ def __reduce__(self):
+ return self.__reduce_ex__(2)
+
+ def __reduce_ex__(self, protocol):
+ return self.__class__, self._getstate(protocol)
+
+ def _cmp(self, other, **kwargs):
+ # Fix for pypy which compares using this method
+ # which would lead to infinite recursion if we didn't override
+ kwargs = {"tzinfo": self.tz}
+
+ if _HAS_FOLD:
+ kwargs["fold"] = self.fold
+
+ dt = datetime.datetime(
+ self.year,
+ self.month,
+ self.day,
+ self.hour,
+ self.minute,
+ self.second,
+ self.microsecond,
+ **kwargs
+ )
+
+ 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)
diff --git a/pendulum/duration.py b/pendulum/duration.py
new file mode 100644
index 0000000..18d0c7f
--- /dev/null
+++ b/pendulum/duration.py
@@ -0,0 +1,479 @@
+from __future__ import absolute_import
+from __future__ import division
+
+from datetime import timedelta
+
+import pendulum
+
+from pendulum.utils._compat import PYPY
+from pendulum.utils._compat import decode
+
+from .constants import SECONDS_PER_DAY
+from .constants import SECONDS_PER_HOUR
+from .constants import SECONDS_PER_MINUTE
+from .constants import US_PER_SECOND
+
+
+def _divide_and_round(a, b):
+ """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)
+ # 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.
+ """
+
+ _y = None
+ _m = None
+ _w = None
+ _d = None
+ _h = None
+ _i = None
+ _s = None
+ _invert = None
+
+ def __new__(
+ cls,
+ days=0,
+ seconds=0,
+ microseconds=0,
+ milliseconds=0,
+ minutes=0,
+ hours=0,
+ weeks=0,
+ years=0,
+ months=0,
+ ):
+ 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
+
+ return self
+
+ def total_minutes(self):
+ return self.total_seconds() / SECONDS_PER_MINUTE
+
+ def total_hours(self):
+ return self.total_seconds() / SECONDS_PER_HOUR
+
+ def total_days(self):
+ return self.total_seconds() / SECONDS_PER_DAY
+
+ def total_weeks(self):
+ return self.total_days() / 7
+
+ if PYPY:
+
+ def total_seconds(self):
+ 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):
+ return self._years
+
+ @property
+ def months(self):
+ return self._months
+
+ @property
+ def weeks(self):
+ return self._weeks
+
+ if PYPY:
+
+ @property
+ def days(self):
+ return self._years * 365 + self._months * 30 + self._days
+
+ @property
+ def remaining_days(self):
+ return self._remaining_days
+
+ @property
+ def hours(self):
+ 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):
+ 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):
+ return self._seconds
+
+ @property
+ def remaining_seconds(self):
+ 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):
+ return self._microseconds
+
+ @property
+ def invert(self):
+ if self._invert is None:
+ self._invert = self.total_seconds() < 0
+
+ return self._invert
+
+ def in_weeks(self):
+ return int(self.total_weeks())
+
+ def in_days(self):
+ return int(self.total_days())
+
+ def in_hours(self):
+ return int(self.total_hours())
+
+ def in_minutes(self):
+ return int(self.total_minutes())
+
+ def in_seconds(self):
+ return int(self.total_seconds())
+
+ def in_words(self, locale=None, separator=" "):
+ """
+ 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.
+ :type locale: str
+
+ :param separator: The separator to use between each unit
+ :type separator: str
+
+ :rtype: str
+ """
+ periods = [
+ ("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()
+
+ locale = pendulum.locale(locale)
+ parts = []
+ for period in periods:
+ unit, count = period
+ if abs(count) > 0:
+ translation = locale.translation(
+ "units.{}.{}".format(unit, locale.plural(abs(count)))
+ )
+ parts.append(translation.format(count))
+
+ if not parts:
+ if abs(self.microseconds) > 0:
+ unit = "units.second.{}".format(locale.plural(1))
+ count = "{:.2f}".format(abs(self.microseconds) / 1e6)
+ else:
+ unit = "units.microsecond.{}".format(locale.plural(0))
+ count = 0
+ translation = locale.translation(unit)
+ parts.append(translation.format(count))
+
+ return decode(separator.join(parts))
+
+ def _sign(self, value):
+ if value < 0:
+ return -1
+
+ return 1
+
+ def as_timedelta(self):
+ """
+ Return the interval as a native timedelta.
+
+ :rtype: timedelta
+ """
+ return timedelta(seconds=self.total_seconds())
+
+ def __str__(self):
+ return self.in_words()
+
+ def __repr__(self):
+ rep = "{}(".format(self.__class__.__name__)
+
+ if self._years:
+ rep += "years={}, ".format(self._years)
+
+ if self._months:
+ rep += "months={}, ".format(self._months)
+
+ if self._weeks:
+ rep += "weeks={}, ".format(self._weeks)
+
+ if self._days:
+ rep += "days={}, ".format(self._remaining_days)
+
+ if self.hours:
+ rep += "hours={}, ".format(self.hours)
+
+ if self.minutes:
+ rep += "minutes={}, ".format(self.minutes)
+
+ if self.remaining_seconds:
+ rep += "seconds={}, ".format(self.remaining_seconds)
+
+ if self.microseconds:
+ rep += "microseconds={}, ".format(self.microseconds)
+
+ rep += ")"
+
+ return rep.replace(", )", ")")
+
+ def __add__(self, other):
+ if isinstance(other, timedelta):
+ return self.__class__(seconds=self.total_seconds() + other.total_seconds())
+
+ return NotImplemented
+
+ __radd__ = __add__
+
+ def __sub__(self, other):
+ if isinstance(other, timedelta):
+ return self.__class__(seconds=self.total_seconds() - other.total_seconds())
+
+ return NotImplemented
+
+ def __neg__(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):
+ return (self._days * (24 * 3600) + self._seconds) * 1000000 + self._microseconds
+
+ def __mul__(self, other):
+ 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__
+
+ def __floordiv__(self, other):
+ if not isinstance(other, (int, timedelta)):
+ return NotImplemented
+
+ usec = self._to_microseconds()
+ if isinstance(other, timedelta):
+ return usec // other._to_microseconds()
+
+ if isinstance(other, int):
+ return self.__class__(
+ 0,
+ 0,
+ usec // other,
+ years=self._years // other,
+ months=self._months // other,
+ )
+
+ def __truediv__(self, other):
+ if not isinstance(other, (int, float, timedelta)):
+ return NotImplemented
+
+ usec = self._to_microseconds()
+ if isinstance(other, timedelta):
+ return usec / other._to_microseconds()
+
+ 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):
+ if isinstance(other, timedelta):
+ r = self._to_microseconds() % other._to_microseconds()
+
+ return self.__class__(0, 0, r)
+
+ return NotImplemented
+
+ def __divmod__(self, other):
+ if isinstance(other, timedelta):
+ q, r = divmod(self._to_microseconds(), other._to_microseconds())
+
+ return q, self.__class__(0, 0, r)
+
+ return NotImplemented
+
+
+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=0,
+ seconds=0,
+ microseconds=0,
+ milliseconds=0,
+ minutes=0,
+ hours=0,
+ weeks=0,
+ years=0,
+ months=0,
+ ):
+ 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)
+ self._seconds = int(total) % SECONDS_PER_DAY
+
+ days = int(total) // SECONDS_PER_DAY
+ self._days = abs(days + years * 365 + months * 30)
+ self._remaining_days = days % 7
+ self._weeks = days // 7
+ self._months = abs(months)
+ self._years = abs(years)
+
+ return self
+
+ def total_seconds(self):
+ return abs(self._total)
+
+ @property
+ def invert(self):
+ if self._invert is None:
+ self._invert = self._total < 0
+
+ return self._invert
diff --git a/pendulum/exceptions.py b/pendulum/exceptions.py
new file mode 100644
index 0000000..6806783
--- /dev/null
+++ b/pendulum/exceptions.py
@@ -0,0 +1,6 @@
+from .parsing.exceptions import ParserError # noqa
+
+
+class PendulumException(Exception):
+
+ pass
diff --git a/pendulum/formatting/__init__.py b/pendulum/formatting/__init__.py
new file mode 100644
index 0000000..a2b47de
--- /dev/null
+++ b/pendulum/formatting/__init__.py
@@ -0,0 +1,4 @@
+from .formatter import Formatter
+
+
+__all__ = ["Formatter"]
diff --git a/pendulum/formatting/difference_formatter.py b/pendulum/formatting/difference_formatter.py
new file mode 100644
index 0000000..3243089
--- /dev/null
+++ b/pendulum/formatting/difference_formatter.py
@@ -0,0 +1,153 @@
+import typing
+
+import pendulum
+
+from pendulum.utils._compat import decode
+
+from ..locales.locale import Locale
+
+
+class DifferenceFormatter(object):
+ """
+ Handles formatting differences in text.
+ """
+
+ def __init__(self, locale="en"):
+ self._locale = Locale.load(locale)
+
+ def format(
+ self, diff, is_now=True, absolute=False, locale=None
+ ): # type: (pendulum.Period, bool, bool, typing.Optional[str]) -> str
+ """
+ Formats a difference.
+
+ :param diff: The difference to format
+ :type diff: pendulum.period.Period
+
+ :param is_now: Whether the difference includes now
+ :type is_now: bool
+
+ :param absolute: Whether it's an absolute difference or not
+ :type absolute: bool
+
+ :param locale: The locale to use
+ :type locale: str or None
+
+ :rtype: str
+ """
+ if locale is None:
+ locale = self._locale
+ else:
+ locale = Locale.load(locale)
+
+ count = diff.remaining_seconds
+
+ 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 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 locale.get(key).format(time)
+ else:
+ unit = "second"
+ count = diff.remaining_seconds
+
+ if count == 0:
+ count = 1
+
+ if absolute:
+ key = "translations.units.{}".format(unit)
+ else:
+ is_future = diff.invert
+
+ if is_now:
+ # Relative to now, so we can use
+ # the CLDR data
+ key = "translations.relative.{}".format(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 += ".{}.future".format(unit)
+ else:
+ key += ".{}.past".format(unit)
+
+ trans = locale.get(key)
+ if not trans:
+ # No special rule
+ time = locale.get(
+ "translations.units.{}.{}".format(unit, locale.plural(count))
+ ).format(count)
+ else:
+ time = trans[locale.plural(count)].format(count)
+
+ key = "custom"
+ if is_future:
+ key += ".after"
+ else:
+ key += ".before"
+
+ return locale.get(key).format(decode(time))
+
+ key += ".{}".format(locale.plural(count))
+
+ return decode(locale.get(key).format(count))
diff --git a/pendulum/formatting/formatter.py b/pendulum/formatting/formatter.py
new file mode 100644
index 0000000..4e493d0
--- /dev/null
+++ b/pendulum/formatting/formatter.py
@@ -0,0 +1,685 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import datetime
+import re
+import typing
+
+import pendulum
+
+from pendulum.locales.locale import Locale
+from pendulum.utils._compat import decode
+
+
+_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 = (
+ r"\[([^\[]*)\]|\\(.)|"
+ "("
+ "Mo|MM?M?M?"
+ "|Do|DDDo|DD?D?D?|ddd?d?|do?"
+ "|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.compile(_TOKENS)
+
+ _FROM_FORMAT_RE = re.compile(r"(?<!\\\[)" + _TOKENS + r"(?!\\\])")
+
+ _LOCALIZABLE_TOKENS = {
+ "Qo": None,
+ "MMMM": "months.wide",
+ "MMM": "months.abbreviated",
+ "Mo": None,
+ "DDDo": None,
+ "Do": lambda locale: tuple(
+ r"\d+{}".format(o) for o in locale.get("custom.ordinal").values()
+ ),
+ "dddd": "days.wide",
+ "ddd": "days.abbreviated",
+ "dd": "days.short",
+ "do": 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 = {
+ # Year
+ "YYYY": lambda dt: "{:d}".format(dt.year),
+ "YY": lambda dt: "{:d}".format(dt.year)[2:],
+ "Y": lambda dt: "{:d}".format(dt.year),
+ # Quarter
+ "Q": lambda dt: "{:d}".format(dt.quarter),
+ # Month
+ "MM": lambda dt: "{:02d}".format(dt.month),
+ "M": lambda dt: "{:d}".format(dt.month),
+ # Day
+ "DD": lambda dt: "{:02d}".format(dt.day),
+ "D": lambda dt: "{:d}".format(dt.day),
+ # Day of Year
+ "DDDD": lambda dt: "{:03d}".format(dt.day_of_year),
+ "DDD": lambda dt: "{:d}".format(dt.day_of_year),
+ # Day of Week
+ "d": lambda dt: "{:d}".format(dt.day_of_week),
+ # Day of ISO Week
+ "E": lambda dt: "{:d}".format(dt.isoweekday()),
+ # Hour
+ "HH": lambda dt: "{:02d}".format(dt.hour),
+ "H": lambda dt: "{:d}".format(dt.hour),
+ "hh": lambda dt: "{:02d}".format(dt.hour % 12 or 12),
+ "h": lambda dt: "{:d}".format(dt.hour % 12 or 12),
+ # Minute
+ "mm": lambda dt: "{:02d}".format(dt.minute),
+ "m": lambda dt: "{:d}".format(dt.minute),
+ # Second
+ "ss": lambda dt: "{:02d}".format(dt.second),
+ "s": lambda dt: "{:d}".format(dt.second),
+ # Fractional second
+ "S": lambda dt: "{:01d}".format(dt.microsecond // 100000),
+ "SS": lambda dt: "{:02d}".format(dt.microsecond // 10000),
+ "SSS": lambda dt: "{:03d}".format(dt.microsecond // 1000),
+ "SSSS": lambda dt: "{:04d}".format(dt.microsecond // 100),
+ "SSSSS": lambda dt: "{:05d}".format(dt.microsecond // 10),
+ "SSSSSS": lambda dt: "{:06d}".format(dt.microsecond),
+ # Timestamp
+ "X": lambda dt: "{:d}".format(dt.int_timestamp),
+ "x": lambda dt: "{:d}".format(dt.int_timestamp * 1000 + dt.microsecond // 1000),
+ # Timezone
+ "zz": lambda dt: "{}".format(dt.tzname() if dt.tzinfo is not None else ""),
+ "z": lambda dt: "{}".format(dt.timezone_name or ""),
+ }
+
+ _DATE_FORMATS = {
+ "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 = {
+ "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 = {
+ "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,
+ "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 = {
+ "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) % 7,
+ "E": lambda weekday: int(weekday),
+ "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, fmt, locale=None
+ ): # type: (pendulum.DateTime, str, typing.Optional[typing.Union[str, Locale]]) -> str
+ """
+ Formats a DateTime instance with a given format and locale.
+
+ :param dt: The instance to format
+ :type dt: pendulum.DateTime
+
+ :param fmt: The format to use
+ :type fmt: str
+
+ :param locale: The locale to use
+ :type locale: str or Locale or None
+
+ :rtype: str
+ """
+ if not locale:
+ locale = pendulum.get_locale()
+
+ locale = Locale.load(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), locale),
+ fmt,
+ )
+
+ return decode(result)
+
+ def _format_token(
+ self, dt, token, locale
+ ): # type: (pendulum.DateTime, str, Locale) -> str
+ """
+ Formats a DateTime instance with a given token and locale.
+
+ :param dt: The instance to format
+ :type dt: pendulum.DateTime
+
+ :param token: The token to use
+ :type token: str
+
+ :param locale: The locale to use
+ :type locale: Locale
+
+ :rtype: str
+ """
+ if token in self._DATE_FORMATS:
+ fmt = locale.get("custom.date_formats.{}".format(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
+
+ if minutes >= 0:
+ sign = "+"
+ else:
+ sign = "-"
+
+ hour, minute = divmod(abs(int(minutes)), 60)
+
+ return "{}{:02d}{}{:02d}".format(sign, hour, separator, minute)
+
+ def _format_localizable_token(
+ self, dt, token, locale
+ ): # type: (pendulum.DateTime, str, Locale) -> str
+ """
+ Formats a DateTime instance
+ with a given localizable token and locale.
+
+ :param dt: The instance to format
+ :type dt: pendulum.DateTime
+
+ :param token: The token to use
+ :type token: str
+
+ :param locale: The locale to use
+ :type locale: Locale
+
+ :rtype: str
+ """
+ if token == "MMM":
+ return locale.get("translations.months.abbreviated")[dt.month]
+ elif token == "MMMM":
+ return locale.get("translations.months.wide")[dt.month]
+ elif token == "dd":
+ return locale.get("translations.days.short")[dt.day_of_week]
+ elif token == "ddd":
+ return locale.get("translations.days.abbreviated")[dt.day_of_week]
+ elif token == "dddd":
+ return locale.get("translations.days.wide")[dt.day_of_week]
+ elif token == "Do":
+ return locale.ordinalize(dt.day)
+ elif token == "do":
+ return locale.ordinalize(dt.day_of_week)
+ 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 == "A":
+ key = "translations.day_periods"
+ if dt.hour >= 12:
+ key += ".pm"
+ else:
+ key += ".am"
+
+ return locale.get(key)
+ else:
+ return token
+
+ def parse(
+ self,
+ time, # type: str
+ fmt, # type: str
+ now, # type: pendulum.DateTime
+ locale=None, # type: typing.Optional[str]
+ ): # type: (...) -> typing.Dict[str, typing.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:
+ return time
+
+ if not locale:
+ locale = pendulum.get_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), locale), escaped_fmt
+ )
+
+ if not re.search("^" + pattern + "$", time):
+ raise ValueError("String does not match format {}".format(fmt))
+
+ re.sub(pattern, lambda m: self._get_parsed_values(m, parsed, locale, now), time)
+
+ return self._check_parsed(parsed, now)
+
+ def _check_parsed(
+ self, parsed, now
+ ): # type: (typing.Dict[str, typing.Any], pendulum.DateTime) -> typing.Dict[str, typing.Any]
+ """
+ Checks validity of parsed elements.
+
+ :param parsed: The elements to parse.
+
+ :return: The validated elements.
+ """
+ validated = {
+ "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("{}".format(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(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 = pendulum.parse(
+ "{}-{:>03d}".format(validated["year"], parsed["day_of_year"])
+ )
+
+ validated["month"] = dt.month
+ validated["day"] = dt.day
+
+ if parsed["day_of_week"] is not None:
+ dt = pendulum.datetime(
+ validated["year"],
+ validated["month"] or now.month,
+ 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
+ if pm:
+ validated["hour"] += 12
+
+ 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, parsed, locale, now
+ ): # type: (typing.Match[str], typing.Dict[str, typing.Any], Locale, 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, value, parsed, now
+ ): # type: (str, str, typing.Dict[str, typing.Any], pendulum.DateTime) -> None
+ parsed_token = self._PARSE_TOKENS[token](value)
+
+ if "Y" in token:
+ if token == "YY":
+ parsed_token = now.year // 100 * 100 + parsed_token
+
+ parsed["year"] = parsed_token
+ elif "Q" == token:
+ 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 = True if value.startswith("-") else False
+ tz = value[1:]
+ if ":" not in tz:
+ if len(tz) == 2:
+ tz = "{}00".format(tz)
+
+ 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, value, parsed, locale
+ ): # type: (str, str, typing.Dict[str, typing.Any], Locale) -> None
+ if token == "MMMM":
+ unit = "month"
+ match = "months.wide"
+ elif token == "MMM":
+ unit = "month"
+ match = "months.abbreviated"
+ elif token == "Do":
+ parsed["day"] = int(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 = list(map(lambda x: x.lower(), valid_values))
+
+ if value not in valid_values:
+ raise ValueError("Invalid date")
+
+ parsed["meridiem"] = ["am", "pm"][valid_values.index(value)]
+
+ return
+ else:
+ raise ValueError('Invalid token "{}"'.format(token))
+
+ parsed[unit] = locale.match_translation(match, value)
+ if value is None:
+ raise ValueError("Invalid date")
+
+ def _replace_tokens(self, token, locale): # type: (str, 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("Unsupported token: {}".format(token))
+
+ if token in self._LOCALIZABLE_TOKENS:
+ values = self._LOCALIZABLE_TOKENS[token]
+ if callable(values):
+ candidates = values(locale)
+ else:
+ candidates = tuple(
+ locale.translation(self._LOCALIZABLE_TOKENS[token]).values()
+ )
+ else:
+ candidates = self._REGEX_TOKENS[token]
+
+ if not candidates:
+ raise ValueError("Unsupported token: {}".format(token))
+
+ if not isinstance(candidates, tuple):
+ candidates = (candidates,)
+
+ pattern = "(?P<{}>{})".format(token, "|".join([decode(p) for p in candidates]))
+
+ return pattern
diff --git a/pendulum/helpers.py b/pendulum/helpers.py
new file mode 100644
index 0000000..f149ca5
--- /dev/null
+++ b/pendulum/helpers.py
@@ -0,0 +1,224 @@
+from __future__ import absolute_import
+
+import os
+import struct
+
+from contextlib import contextmanager
+from datetime import date
+from datetime import datetime
+from datetime import timedelta
+from math import copysign
+from typing import TYPE_CHECKING
+from typing import Iterator
+from typing import Optional
+from typing import TypeVar
+from typing import overload
+
+import pendulum
+
+from .constants import DAYS_PER_MONTHS
+from .formatting.difference_formatter import DifferenceFormatter
+from .locales.locale import Locale
+
+
+if TYPE_CHECKING:
+ # Prevent import cycles
+ from .period import Period
+
+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 ._extensions._helpers import local_time
+ from ._extensions._helpers import precise_diff
+ from ._extensions._helpers import is_leap
+ from ._extensions._helpers import is_long_year
+ from ._extensions._helpers import week_day
+ from ._extensions._helpers import days_in_year
+ from ._extensions._helpers import timestamp
+except ImportError:
+ from ._extensions.helpers import local_time # noqa
+ from ._extensions.helpers import precise_diff # noqa
+ from ._extensions.helpers import is_leap # noqa
+ from ._extensions.helpers import is_long_year # noqa
+ from ._extensions.helpers import week_day # noqa
+ from ._extensions.helpers import days_in_year # noqa
+ from ._extensions.helpers import timestamp # noqa
+
+
+difference_formatter = DifferenceFormatter()
+
+
+@overload
+def add_duration(
+ dt, # type: _DT
+ years=0, # type: int
+ months=0, # type: int
+ weeks=0, # type: int
+ days=0, # type: int
+ hours=0, # type: int
+ minutes=0, # type: int
+ seconds=0, # type: int
+ microseconds=0, # type: int
+): # type: (...) -> _DT
+ pass
+
+
+@overload
+def add_duration(
+ dt, # type: _D
+ years=0, # type: int
+ months=0, # type: int
+ weeks=0, # type: int
+ days=0, # type: int
+): # type: (...) -> _D
+ pass
+
+
+def add_duration(
+ dt,
+ years=0,
+ months=0,
+ weeks=0,
+ days=0,
+ hours=0,
+ minutes=0,
+ seconds=0,
+ microseconds=0,
+):
+ """
+ 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)
+ 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, is_now=True, absolute=False, locale=None
+): # type: (Period, bool, bool, Optional[str]) -> str
+ if locale is None:
+ locale = get_locale()
+
+ return difference_formatter.format(diff, is_now, absolute, locale)
+
+
+def _sign(x):
+ return int(copysign(1, x))
+
+
+# Global helpers
+
+
+@contextmanager
+def test(mock): # type: (pendulum.DateTime) -> Iterator[None]
+ set_test_now(mock)
+ try:
+ yield
+ finally:
+ set_test_now()
+
+
+def set_test_now(test_now=None): # type: (Optional[pendulum.DateTime]) -> None
+ pendulum._TEST_NOW = test_now
+
+
+def get_test_now(): # type: () -> Optional[pendulum.DateTime]
+ return pendulum._TEST_NOW
+
+
+def has_test_now(): # type: () -> bool
+ return pendulum._TEST_NOW is not None
+
+
+def locale(name): # type: (str) -> Locale
+ return Locale.load(name)
+
+
+def set_locale(name): # type: (str) -> None
+ locale(name)
+
+ pendulum._LOCALE = name
+
+
+def get_locale(): # type: () -> str
+ return pendulum._LOCALE
+
+
+def week_starts_at(wday): # type: (int) -> None
+ if wday < pendulum.SUNDAY or wday > pendulum.SATURDAY:
+ raise ValueError("Invalid week day as start of week.")
+
+ pendulum._WEEK_STARTS_AT = wday
+
+
+def week_ends_at(wday): # type: (int) -> None
+ if wday < pendulum.SUNDAY or wday > pendulum.SATURDAY:
+ raise ValueError("Invalid week day as start of week.")
+
+ pendulum._WEEK_ENDS_AT = wday
diff --git a/pendulum/locales/__init__.py b/pendulum/locales/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/__init__.py
diff --git a/pendulum/locales/da/__init__.py b/pendulum/locales/da/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/da/__init__.py
diff --git a/pendulum/locales/da/custom.py b/pendulum/locales/da/custom.py
new file mode 100644
index 0000000..258e47b
--- /dev/null
+++ b/pendulum/locales/da/custom.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+da custom locale file.
+"""
+
+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/pendulum/locales/da/locale.py b/pendulum/locales/da/locale.py
new file mode 100644
index 0000000..b829e34
--- /dev/null
+++ b/pendulum/locales/da/locale.py
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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: "søn.",
+ 1: "man.",
+ 2: "tir.",
+ 3: "ons.",
+ 4: "tor.",
+ 5: "fre.",
+ 6: "lør.",
+ },
+ "narrow": {0: "S", 1: "M", 2: "T", 3: "O", 4: "T", 5: "F", 6: "L"},
+ "short": {0: "sø", 1: "ma", 2: "ti", 3: "on", 4: "to", 5: "fr", 6: "lø"},
+ "wide": {
+ 0: "søndag",
+ 1: "mandag",
+ 2: "tirsdag",
+ 3: "onsdag",
+ 4: "torsdag",
+ 5: "fredag",
+ 6: "lørdag",
+ },
+ },
+ "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",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/de/__init__.py b/pendulum/locales/de/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/de/__init__.py
diff --git a/pendulum/locales/de/custom.py b/pendulum/locales/de/custom.py
new file mode 100644
index 0000000..3024f0b
--- /dev/null
+++ b/pendulum/locales/de/custom.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+de custom locale file.
+"""
+
+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/pendulum/locales/de/locale.py b/pendulum/locales/de/locale.py
new file mode 100644
index 0000000..b180fc5
--- /dev/null
+++ b/pendulum/locales/de/locale.py
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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: "So.",
+ 1: "Mo.",
+ 2: "Di.",
+ 3: "Mi.",
+ 4: "Do.",
+ 5: "Fr.",
+ 6: "Sa.",
+ },
+ "narrow": {0: "S", 1: "M", 2: "D", 3: "M", 4: "D", 5: "F", 6: "S"},
+ "short": {
+ 0: "So.",
+ 1: "Mo.",
+ 2: "Di.",
+ 3: "Mi.",
+ 4: "Do.",
+ 5: "Fr.",
+ 6: "Sa.",
+ },
+ "wide": {
+ 0: "Sonntag",
+ 1: "Montag",
+ 2: "Dienstag",
+ 3: "Mittwoch",
+ 4: "Donnerstag",
+ 5: "Freitag",
+ 6: "Samstag",
+ },
+ },
+ "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",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/en/__init__.py b/pendulum/locales/en/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/en/__init__.py
diff --git a/pendulum/locales/en/custom.py b/pendulum/locales/en/custom.py
new file mode 100644
index 0000000..de224e0
--- /dev/null
+++ b/pendulum/locales/en/custom.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+en custom locale file.
+"""
+
+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/pendulum/locales/en/locale.py b/pendulum/locales/en/locale.py
new file mode 100644
index 0000000..acee4d2
--- /dev/null
+++ b/pendulum/locales/en/locale.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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: "Sun",
+ 1: "Mon",
+ 2: "Tue",
+ 3: "Wed",
+ 4: "Thu",
+ 5: "Fri",
+ 6: "Sat",
+ },
+ "narrow": {0: "S", 1: "M", 2: "T", 3: "W", 4: "T", 5: "F", 6: "S"},
+ "short": {0: "Su", 1: "Mo", 2: "Tu", 3: "We", 4: "Th", 5: "Fr", 6: "Sa"},
+ "wide": {
+ 0: "Sunday",
+ 1: "Monday",
+ 2: "Tuesday",
+ 3: "Wednesday",
+ 4: "Thursday",
+ 5: "Friday",
+ 6: "Saturday",
+ },
+ },
+ "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",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/es/__init__.py b/pendulum/locales/es/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/es/__init__.py
diff --git a/pendulum/locales/es/custom.py b/pendulum/locales/es/custom.py
new file mode 100644
index 0000000..5862f7e
--- /dev/null
+++ b/pendulum/locales/es/custom.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+es custom locale file.
+"""
+
+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/pendulum/locales/es/locale.py b/pendulum/locales/es/locale.py
new file mode 100644
index 0000000..f385e4c
--- /dev/null
+++ b/pendulum/locales/es/locale.py
@@ -0,0 +1,144 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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: "dom.",
+ 1: "lun.",
+ 2: "mar.",
+ 3: "mié.",
+ 4: "jue.",
+ 5: "vie.",
+ 6: "sáb.",
+ },
+ "narrow": {0: "D", 1: "L", 2: "M", 3: "X", 4: "J", 5: "V", 6: "S"},
+ "short": {0: "DO", 1: "LU", 2: "MA", 3: "MI", 4: "JU", 5: "VI", 6: "SA"},
+ "wide": {
+ 0: "domingo",
+ 1: "lunes",
+ 2: "martes",
+ 3: "miércoles",
+ 4: "jueves",
+ 5: "viernes",
+ 6: "sábado",
+ },
+ },
+ "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",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/fa/__init__.py b/pendulum/locales/fa/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/fa/__init__.py
diff --git a/pendulum/locales/fa/custom.py b/pendulum/locales/fa/custom.py
new file mode 100644
index 0000000..fa5a7c1
--- /dev/null
+++ b/pendulum/locales/fa/custom.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+fa custom locale file.
+"""
+
+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/pendulum/locales/fa/locale.py b/pendulum/locales/fa/locale.py
new file mode 100644
index 0000000..f18b0f6
--- /dev/null
+++ b/pendulum/locales/fa/locale.py
@@ -0,0 +1,138 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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: "دوشنبه",
+ 2: "سه\u200cشنبه",
+ 3: "چهارشنبه",
+ 4: "پنجشنبه",
+ 5: "جمعه",
+ 6: "شنبه",
+ },
+ "narrow": {0: "ی", 1: "د", 2: "س", 3: "چ", 4: "پ", 5: "ج", 6: "ش"},
+ "short": {0: "۱ش", 1: "۲ش", 2: "۳ش", 3: "۴ش", 4: "۵ش", 5: "ج", 6: "ش"},
+ "wide": {
+ 0: "یکشنبه",
+ 1: "دوشنبه",
+ 2: "سه\u200cشنبه",
+ 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": "شب",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/fo/__init__.py b/pendulum/locales/fo/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/fo/__init__.py
diff --git a/pendulum/locales/fo/custom.py b/pendulum/locales/fo/custom.py
new file mode 100644
index 0000000..946ab19
--- /dev/null
+++ b/pendulum/locales/fo/custom.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+fo custom locale file.
+"""
+
+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/pendulum/locales/fo/locale.py b/pendulum/locales/fo/locale.py
new file mode 100644
index 0000000..345f524
--- /dev/null
+++ b/pendulum/locales/fo/locale.py
@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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: "sun.",
+ 1: "mán.",
+ 2: "týs.",
+ 3: "mik.",
+ 4: "hós.",
+ 5: "frí.",
+ 6: "ley.",
+ },
+ "narrow": {0: "S", 1: "M", 2: "T", 3: "M", 4: "H", 5: "F", 6: "L"},
+ "short": {
+ 0: "su.",
+ 1: "má.",
+ 2: "tý.",
+ 3: "mi.",
+ 4: "hó.",
+ 5: "fr.",
+ 6: "le.",
+ },
+ "wide": {
+ 0: "sunnudagur",
+ 1: "mánadagur",
+ 2: "týsdagur",
+ 3: "mikudagur",
+ 4: "hósdagur",
+ 5: "fríggjadagur",
+ 6: "leygardagur",
+ },
+ },
+ "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"},
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/fr/__init__.py b/pendulum/locales/fr/__init__.py
new file mode 100644
index 0000000..4c48b5a
--- /dev/null
+++ b/pendulum/locales/fr/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/pendulum/locales/fr/custom.py b/pendulum/locales/fr/custom.py
new file mode 100644
index 0000000..0edddbf
--- /dev/null
+++ b/pendulum/locales/fr/custom.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+fr custom locale file.
+"""
+
+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/pendulum/locales/fr/locale.py b/pendulum/locales/fr/locale.py
new file mode 100644
index 0000000..137c012
--- /dev/null
+++ b/pendulum/locales/fr/locale.py
@@ -0,0 +1,136 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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: "dim.",
+ 1: "lun.",
+ 2: "mar.",
+ 3: "mer.",
+ 4: "jeu.",
+ 5: "ven.",
+ 6: "sam.",
+ },
+ "narrow": {0: "D", 1: "L", 2: "M", 3: "M", 4: "J", 5: "V", 6: "S"},
+ "short": {0: "di", 1: "lu", 2: "ma", 3: "me", 4: "je", 5: "ve", 6: "sa"},
+ "wide": {
+ 0: "dimanche",
+ 1: "lundi",
+ 2: "mardi",
+ 3: "mercredi",
+ 4: "jeudi",
+ 5: "vendredi",
+ 6: "samedi",
+ },
+ },
+ "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",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/id/__init__.py b/pendulum/locales/id/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/id/__init__.py
diff --git a/pendulum/locales/id/custom.py b/pendulum/locales/id/custom.py
new file mode 100644
index 0000000..2202481
--- /dev/null
+++ b/pendulum/locales/id/custom.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+id custom locale file.
+"""
+
+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/pendulum/locales/id/locale.py b/pendulum/locales/id/locale.py
new file mode 100644
index 0000000..5a3485e
--- /dev/null
+++ b/pendulum/locales/id/locale.py
@@ -0,0 +1,144 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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: "Min",
+ 1: "Sen",
+ 2: "Sel",
+ 3: "Rab",
+ 4: "Kam",
+ 5: "Jum",
+ 6: "Sab",
+ },
+ "narrow": {0: "M", 1: "S", 2: "S", 3: "R", 4: "K", 5: "J", 6: "S"},
+ "short": {
+ 0: "Min",
+ 1: "Sen",
+ 2: "Sel",
+ 3: "Rab",
+ 4: "Kam",
+ 5: "Jum",
+ 6: "Sab",
+ },
+ "wide": {
+ 0: "Minggu",
+ 1: "Senin",
+ 2: "Selasa",
+ 3: "Rabu",
+ 4: "Kamis",
+ 5: "Jumat",
+ 6: "Sabtu",
+ },
+ },
+ "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",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/it/__init__.py b/pendulum/locales/it/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/it/__init__.py
diff --git a/pendulum/locales/it/custom.py b/pendulum/locales/it/custom.py
new file mode 100644
index 0000000..744f55c
--- /dev/null
+++ b/pendulum/locales/it/custom.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+"""
+it custom locale file.
+"""
+
+from __future__ import unicode_literals
+
+
+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/pendulum/locales/it/locale.py b/pendulum/locales/it/locale.py
new file mode 100644
index 0000000..4abf717
--- /dev/null
+++ b/pendulum/locales/it/locale.py
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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: "dom",
+ 1: "lun",
+ 2: "mar",
+ 3: "mer",
+ 4: "gio",
+ 5: "ven",
+ 6: "sab",
+ },
+ "narrow": {0: "D", 1: "L", 2: "M", 3: "M", 4: "G", 5: "V", 6: "S"},
+ "short": {
+ 0: "dom",
+ 1: "lun",
+ 2: "mar",
+ 3: "mer",
+ 4: "gio",
+ 5: "ven",
+ 6: "sab",
+ },
+ "wide": {
+ 0: "domenica",
+ 1: "lunedì",
+ 2: "martedì",
+ 3: "mercoledì",
+ 4: "giovedì",
+ 5: "venerdì",
+ 6: "sabato",
+ },
+ },
+ "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",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/ko/__init__.py b/pendulum/locales/ko/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/ko/__init__.py
diff --git a/pendulum/locales/ko/custom.py b/pendulum/locales/ko/custom.py
new file mode 100644
index 0000000..beac040
--- /dev/null
+++ b/pendulum/locales/ko/custom.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+ko custom locale file.
+"""
+
+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/pendulum/locales/ko/locale.py b/pendulum/locales/ko/locale.py
new file mode 100644
index 0000000..3c81b0e
--- /dev/null
+++ b/pendulum/locales/ko/locale.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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": "밤",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/locale.py b/pendulum/locales/locale.py
new file mode 100644
index 0000000..154db42
--- /dev/null
+++ b/pendulum/locales/locale.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import os
+import re
+
+from importlib import import_module
+from typing import Any
+from typing import Optional
+from typing import Union
+
+from pendulum.utils._compat import basestring
+from pendulum.utils._compat import decode
+
+
+class Locale:
+ """
+ Represent a specific locale.
+ """
+
+ _cache = {}
+
+ def __init__(self, locale, data): # type: (str, Any) -> None
+ self._locale = locale
+ self._data = data
+ self._key_cache = {}
+
+ @classmethod
+ def load(cls, locale): # type: (Union[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 = os.path.join(os.path.dirname(__file__), actual_locale)
+ while not os.path.exists(locale_path):
+ if actual_locale == locale:
+ raise ValueError("Locale [{}] does not exist.".format(locale))
+
+ actual_locale = actual_locale.split("_")[0]
+
+ m = import_module("pendulum.locales.{}.locale".format(actual_locale))
+
+ cls._cache[locale] = cls(locale, m.locale)
+
+ return cls._cache[locale]
+
+ @classmethod
+ def normalize_locale(cls, locale): # type: (str) -> str
+ m = re.match("([a-z]{2})[-_]([a-z]{2})", locale, re.I)
+ if m:
+ return "{}_{}".format(m.group(1).lower(), m.group(2).lower())
+ else:
+ return locale.lower()
+
+ def get(self, key, default=None): # type: (str, Optional[Any]) -> 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
+
+ if isinstance(result, basestring):
+ result = decode(result)
+
+ self._key_cache[key] = result
+
+ return self._key_cache[key]
+
+ def translation(self, key): # type: (str) -> Any
+ return self.get("translations.{}".format(key))
+
+ def plural(self, number): # type: (int) -> str
+ return decode(self._data["plural"](number))
+
+ def ordinal(self, number): # type: (int) -> str
+ return decode(self._data["ordinal"](number))
+
+ def ordinalize(self, number): # type: (int) -> str
+ ordinal = self.get("custom.ordinal.{}".format(self.ordinal(number)))
+
+ if not ordinal:
+ return decode("{}".format(number))
+
+ return decode("{}{}".format(number, ordinal))
+
+ def match_translation(self, key, value):
+ translations = self.translation(key)
+ if value not in translations.values():
+ return None
+
+ return {v: k for k, v in translations.items()}[value]
+
+ def __repr__(self):
+ return "{}('{}')".format(self.__class__.__name__, self._locale)
diff --git a/pendulum/locales/lt/__init__.py b/pendulum/locales/lt/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/lt/__init__.py
diff --git a/pendulum/locales/lt/custom.py b/pendulum/locales/lt/custom.py
new file mode 100644
index 0000000..addaaf8
--- /dev/null
+++ b/pendulum/locales/lt/custom.py
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+lt custom locale file.
+"""
+
+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/pendulum/locales/lt/locale.py b/pendulum/locales/lt/locale.py
new file mode 100644
index 0000000..12451b6
--- /dev/null
+++ b/pendulum/locales/lt/locale.py
@@ -0,0 +1,258 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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: "sk",
+ 1: "pr",
+ 2: "an",
+ 3: "tr",
+ 4: "kt",
+ 5: "pn",
+ 6: "št",
+ },
+ "narrow": {0: "S", 1: "P", 2: "A", 3: "T", 4: "K", 5: "P", 6: "Š"},
+ "short": {0: "Sk", 1: "Pr", 2: "An", 3: "Tr", 4: "Kt", 5: "Pn", 6: "Št"},
+ "wide": {
+ 0: "sekmadienis",
+ 1: "pirmadienis",
+ 2: "antradienis",
+ 3: "trečiadienis",
+ 4: "ketvirtadienis",
+ 5: "penktadienis",
+ 6: "šeštadienis",
+ },
+ },
+ "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",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/nb/__init__.py b/pendulum/locales/nb/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/nb/__init__.py
diff --git a/pendulum/locales/nb/custom.py b/pendulum/locales/nb/custom.py
new file mode 100644
index 0000000..666f1b4
--- /dev/null
+++ b/pendulum/locales/nb/custom.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+nn custom locale file.
+"""
+
+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/pendulum/locales/nb/locale.py b/pendulum/locales/nb/locale.py
new file mode 100644
index 0000000..9ef9160
--- /dev/null
+++ b/pendulum/locales/nb/locale.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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: "søn.",
+ 1: "man.",
+ 2: "tir.",
+ 3: "ons.",
+ 4: "tor.",
+ 5: "fre.",
+ 6: "lør.",
+ },
+ "narrow": {0: "S", 1: "M", 2: "T", 3: "O", 4: "T", 5: "F", 6: "L"},
+ "short": {
+ 0: "sø.",
+ 1: "ma.",
+ 2: "ti.",
+ 3: "on.",
+ 4: "to.",
+ 5: "fr.",
+ 6: "lø.",
+ },
+ "wide": {
+ 0: "søndag",
+ 1: "mandag",
+ 2: "tirsdag",
+ 3: "onsdag",
+ 4: "torsdag",
+ 5: "fredag",
+ 6: "lørdag",
+ },
+ },
+ "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",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/nl/__init__.py b/pendulum/locales/nl/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/nl/__init__.py
diff --git a/pendulum/locales/nl/custom.py b/pendulum/locales/nl/custom.py
new file mode 100644
index 0000000..c957cda
--- /dev/null
+++ b/pendulum/locales/nl/custom.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+nl custom locale file.
+"""
+
+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/pendulum/locales/nl/locale.py b/pendulum/locales/nl/locale.py
new file mode 100644
index 0000000..270f18e
--- /dev/null
+++ b/pendulum/locales/nl/locale.py
@@ -0,0 +1,137 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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: "zo",
+ 1: "ma",
+ 2: "di",
+ 3: "wo",
+ 4: "do",
+ 5: "vr",
+ 6: "za",
+ },
+ "narrow": {0: "Z", 1: "M", 2: "D", 3: "W", 4: "D", 5: "V", 6: "Z"},
+ "short": {0: "zo", 1: "ma", 2: "di", 3: "wo", 4: "do", 5: "vr", 6: "za"},
+ "wide": {
+ 0: "zondag",
+ 1: "maandag",
+ 2: "dinsdag",
+ 3: "woensdag",
+ 4: "donderdag",
+ 5: "vrijdag",
+ 6: "zaterdag",
+ },
+ },
+ "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",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/nn/__init__.py b/pendulum/locales/nn/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/nn/__init__.py
diff --git a/pendulum/locales/nn/custom.py b/pendulum/locales/nn/custom.py
new file mode 100644
index 0000000..666f1b4
--- /dev/null
+++ b/pendulum/locales/nn/custom.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+nn custom locale file.
+"""
+
+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/pendulum/locales/nn/locale.py b/pendulum/locales/nn/locale.py
new file mode 100644
index 0000000..7236d0c
--- /dev/null
+++ b/pendulum/locales/nn/locale.py
@@ -0,0 +1,144 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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: "søn.",
+ 1: "mån.",
+ 2: "tys.",
+ 3: "ons.",
+ 4: "tor.",
+ 5: "fre.",
+ 6: "lau.",
+ },
+ "narrow": {0: "S", 1: "M", 2: "T", 3: "O", 4: "T", 5: "F", 6: "L"},
+ "short": {
+ 0: "sø.",
+ 1: "må.",
+ 2: "ty.",
+ 3: "on.",
+ 4: "to.",
+ 5: "fr.",
+ 6: "la.",
+ },
+ "wide": {
+ 0: "søndag",
+ 1: "måndag",
+ 2: "tysdag",
+ 3: "onsdag",
+ 4: "torsdag",
+ 5: "fredag",
+ 6: "laurdag",
+ },
+ },
+ "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"},
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/pl/__init__.py b/pendulum/locales/pl/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/pl/__init__.py
diff --git a/pendulum/locales/pl/custom.py b/pendulum/locales/pl/custom.py
new file mode 100644
index 0000000..dc20eb8
--- /dev/null
+++ b/pendulum/locales/pl/custom.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+pl custom locale file.
+"""
+
+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/pendulum/locales/pl/locale.py b/pendulum/locales/pl/locale.py
new file mode 100644
index 0000000..e603efb
--- /dev/null
+++ b/pendulum/locales/pl/locale.py
@@ -0,0 +1,282 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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: "niedz.",
+ 1: "pon.",
+ 2: "wt.",
+ 3: "śr.",
+ 4: "czw.",
+ 5: "pt.",
+ 6: "sob.",
+ },
+ "narrow": {0: "n", 1: "p", 2: "w", 3: "ś", 4: "c", 5: "p", 6: "s"},
+ "short": {
+ 0: "nie",
+ 1: "pon",
+ 2: "wto",
+ 3: "śro",
+ 4: "czw",
+ 5: "pią",
+ 6: "sob",
+ },
+ "wide": {
+ 0: "niedziela",
+ 1: "poniedziałek",
+ 2: "wtorek",
+ 3: "środa",
+ 4: "czwartek",
+ 5: "piątek",
+ 6: "sobota",
+ },
+ },
+ "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",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/pt_br/__init__.py b/pendulum/locales/pt_br/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/pt_br/__init__.py
diff --git a/pendulum/locales/pt_br/custom.py b/pendulum/locales/pt_br/custom.py
new file mode 100644
index 0000000..3cc3f0d
--- /dev/null
+++ b/pendulum/locales/pt_br/custom.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+pt-br custom locale file.
+"""
+
+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/pendulum/locales/pt_br/locale.py b/pendulum/locales/pt_br/locale.py
new file mode 100644
index 0000000..c70c671
--- /dev/null
+++ b/pendulum/locales/pt_br/locale.py
@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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: "dom",
+ 1: "seg",
+ 2: "ter",
+ 3: "qua",
+ 4: "qui",
+ 5: "sex",
+ 6: "sáb",
+ },
+ "narrow": {0: "D", 1: "S", 2: "T", 3: "Q", 4: "Q", 5: "S", 6: "S"},
+ "short": {
+ 0: "dom",
+ 1: "seg",
+ 2: "ter",
+ 3: "qua",
+ 4: "qui",
+ 5: "sex",
+ 6: "sáb",
+ },
+ "wide": {
+ 0: "domingo",
+ 1: "segunda-feira",
+ 2: "terça-feira",
+ 3: "quarta-feira",
+ 4: "quinta-feira",
+ 5: "sexta-feira",
+ 6: "sábado",
+ },
+ },
+ "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",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/ru/__init__.py b/pendulum/locales/ru/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/ru/__init__.py
diff --git a/pendulum/locales/ru/custom.py b/pendulum/locales/ru/custom.py
new file mode 100644
index 0000000..ed770c3
--- /dev/null
+++ b/pendulum/locales/ru/custom.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+ru custom locale file.
+"""
+
+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/pendulum/locales/ru/locale.py b/pendulum/locales/ru/locale.py
new file mode 100644
index 0000000..8c7d53b
--- /dev/null
+++ b/pendulum/locales/ru/locale.py
@@ -0,0 +1,273 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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": "ночи",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/zh/__init__.py b/pendulum/locales/zh/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/zh/__init__.py
diff --git a/pendulum/locales/zh/custom.py b/pendulum/locales/zh/custom.py
new file mode 100644
index 0000000..7b35d66
--- /dev/null
+++ b/pendulum/locales/zh/custom.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+"""
+zh custom locale file.
+"""
+
+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/pendulum/locales/zh/locale.py b/pendulum/locales/zh/locale.py
new file mode 100644
index 0000000..ea04fc3
--- /dev/null
+++ b/pendulum/locales/zh/locale.py
@@ -0,0 +1,116 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .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": "凌晨",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/mixins/__init__.py b/pendulum/mixins/__init__.py
new file mode 100644
index 0000000..4c48b5a
--- /dev/null
+++ b/pendulum/mixins/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/pendulum/mixins/default.py b/pendulum/mixins/default.py
new file mode 100644
index 0000000..bfb5912
--- /dev/null
+++ b/pendulum/mixins/default.py
@@ -0,0 +1,43 @@
+from ..formatting import Formatter
+
+
+_formatter = Formatter()
+
+
+class FormattableMixin(object):
+
+ _formatter = _formatter
+
+ def format(self, fmt, locale=None):
+ """
+ Formats the instance using the given format.
+
+ :param fmt: The format to use
+ :type fmt: str
+
+ :param locale: The locale to use
+ :type locale: str or None
+
+ :rtype: str
+ """
+ return self._formatter.format(self, fmt, locale)
+
+ def for_json(self):
+ """
+ Methods for automatic json serialization by simplejson
+
+ :rtype: str
+ """
+ return str(self)
+
+ def __format__(self, format_spec):
+ 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):
+ return self.isoformat()
diff --git a/pendulum/parser.py b/pendulum/parser.py
new file mode 100644
index 0000000..9b9e383
--- /dev/null
+++ b/pendulum/parser.py
@@ -0,0 +1,121 @@
+from __future__ import absolute_import
+
+import datetime
+import typing
+
+import pendulum
+
+from .date import Date
+from .datetime import DateTime
+from .parsing import _Interval
+from .parsing import parse as base_parse
+from .time import Duration
+from .time import Time
+from .tz import UTC
+
+
+try:
+ from .parsing._iso8601 import Duration as CDuration
+except ImportError:
+ CDuration = None
+
+
+def parse(
+ text, **options
+): # type: (str, **typing.Any) -> typing.Union[Date, Time, DateTime, Duration]
+ # Use the mock now value if it exists
+ options["now"] = options.get("now", pendulum.get_test_now())
+
+ return _parse(text, **options)
+
+
+def _parse(text, **options):
+ """
+ Parses a string with the given options.
+
+ :param text: The string to parse.
+ :type text: str
+
+ :rtype: mixed
+ """
+ # 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.period(
+ 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(parsed.end, tz=options.get("tz", UTC))
+
+ return pendulum.period(
+ 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.period(
+ pendulum.instance(parsed.start, tz=options.get("tz", UTC)),
+ pendulum.instance(parsed.end, tz=options.get("tz", UTC)),
+ )
+
+ if CDuration and isinstance(parsed, CDuration):
+ 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,
+ )
+
+ return parsed
diff --git a/pendulum/parsing/__init__.py b/pendulum/parsing/__init__.py
new file mode 100644
index 0000000..400f119
--- /dev/null
+++ b/pendulum/parsing/__init__.py
@@ -0,0 +1,234 @@
+import copy
+import os
+import re
+import struct
+
+from datetime import date
+from datetime import datetime
+from datetime import time
+
+from dateutil import parser
+
+from .exceptions import ParserError
+
+
+with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1"
+
+try:
+ if not with_extensions or struct.calcsize("P") == 4:
+ raise ImportError()
+
+ from ._iso8601 import parse_iso8601
+except ImportError:
+ from .iso8601 import parse_iso8601
+
+
+COMMON = re.compile(
+ # Date (optional)
+ "^"
+ "(?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)
+ "(?P<time>"
+ r" (?P<timesep>\ )?" # Separator (space)
+ r" (?P<hour>\d{1,2}):(?P<minute>\d{1,2})?(?::(?P<second>\d{1,2}))?" # HH:mm:ss (optional mm and ss)
+ # 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, **options):
+ """
+ Parses a string with the given options.
+
+ :param text: The string to parse.
+ :type text: str
+
+ :rtype: Parsed
+ """
+ _options = copy.copy(DEFAULT_OPTIONS)
+ _options.update(options)
+
+ return _normalize(_parse(text, **_options), **_options)
+
+
+def _normalize(parsed, **options):
+ """
+ Normalizes the parsed element.
+
+ :param parsed: The parsed elements.
+ :type parsed: Parsed
+
+ :rtype: Parsed
+ """
+ if options.get("exact"):
+ return parsed
+
+ if isinstance(parsed, time):
+ now = 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, **options):
+ # Trying to parse ISO8601
+ try:
+ return parse_iso8601(text)
+ except ValueError:
+ pass
+
+ try:
+ return _parse_iso8601_interval(text)
+ except ValueError:
+ pass
+
+ try:
+ return _parse_common(text, **options)
+ except ParserError:
+ pass
+
+ # We couldn't parse the string
+ # so we fallback on the dateutil parser
+ # If not strict
+ if options.get("strict", True):
+ raise ParserError("Unable to parse string [{}]".format(text))
+
+ try:
+ dt = parser.parse(
+ text, dayfirst=options["day_first"], yearfirst=options["year_first"]
+ )
+ except ValueError:
+ raise ParserError("Invalid date string: {}".format(text))
+
+ return dt
+
+
+def _parse_common(text, **options):
+ """
+ Tries to parse the string as a common datetime format.
+
+ :param text: The string to parse.
+ :type text: str
+
+ :rtype: dict or None
+ """
+ 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"))
+
+ if m.group("second"):
+ second = int(m.group("second"))
+ else:
+ second = 0
+
+ # Grabbing subseconds, if any
+ microsecond = 0
+ if m.group("subsecondsection"):
+ # Limiting to 6 chars
+ subsecond = m.group("subsecond")[:6]
+
+ microsecond = int("{:0<6}".format(subsecond))
+
+ 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=None, end=None, duration=None):
+ self.start = start
+ self.end = end
+ self.duration = duration
+
+
+def _parse_iso8601_interval(text):
+ 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(start, end, duration)
diff --git a/pendulum/parsing/_iso8601.c b/pendulum/parsing/_iso8601.c
new file mode 100644
index 0000000..2e14e4b
--- /dev/null
+++ b/pendulum/parsing/_iso8601.c
@@ -0,0 +1,1371 @@
+/* ------------------------------------------------------------------------- */
+
+#include <Python.h>
+#include <datetime.h>
+#include <structmember.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+#ifndef PyVarObject_HEAD_INIT
+#define PyVarObject_HEAD_INIT(type, size) PyObject_HEAD_INIT(type) size,
+#endif
+
+
+/* ------------------------------------------------------------------------- */
+
+#define EPOCH_YEAR 1970
+
+#define DAYS_PER_N_YEAR 365
+#define DAYS_PER_L_YEAR 366
+
+#define USECS_PER_SEC 1000000
+
+#define SECS_PER_MIN 60
+#define SECS_PER_HOUR (60 * SECS_PER_MIN)
+#define SECS_PER_DAY (SECS_PER_HOUR * 24)
+
+// 400-year chunks always have 146097 days (20871 weeks).
+#define DAYS_PER_400_YEARS 146097L
+#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)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.
+const int64_t SECS_PER_100_YEARS[2] = {
+ (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY,
+ (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * 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.
+const int32_t SECS_PER_4_YEARS[2] = {
+ (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.
+const int32_t SECS_PER_YEAR[2] = {
+ DAYS_PER_N_YEAR * SECS_PER_DAY,
+ DAYS_PER_L_YEAR * SECS_PER_DAY
+};
+
+#define MONTHS_PER_YEAR 12
+
+// The month lengths in non-leap and leap years respectively.
+const int32_t DAYS_PER_MONTHS[2][13] = {
+ {-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.
+const int32_t MONTHS_OFFSETS[2][14] = {
+ {-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}
+};
+
+const int DAY_OF_WEEK_TABLE[12] = {
+ 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4
+};
+
+#define TM_SUNDAY 0
+#define TM_MONDAY 1
+#define TM_TUESDAY 2
+#define TM_WEDNESDAY 3
+#define TM_THURSDAY 4
+#define TM_FRIDAY 5
+#define TM_SATURDAY 6
+
+#define TM_JANUARY 0
+#define TM_FEBRUARY 1
+#define TM_MARCH 2
+#define TM_APRIL 3
+#define TM_MAY 4
+#define TM_JUNE 5
+#define TM_JULY 6
+#define TM_AUGUST 7
+#define TM_SEPTEMBER 8
+#define TM_OCTOBER 9
+#define TM_NOVEMBER 10
+#define TM_DECEMBER 11
+
+// Parsing errors
+const int PARSER_INVALID_ISO8601 = 0;
+const int PARSER_INVALID_DATE = 1;
+const int PARSER_INVALID_TIME = 2;
+const int PARSER_INVALID_WEEK_DATE = 3;
+const int PARSER_INVALID_WEEK_NUMBER = 4;
+const int PARSER_INVALID_WEEKDAY_NUMBER = 5;
+const int PARSER_INVALID_ORDINAL_DAY_FOR_YEAR = 6;
+const int PARSER_INVALID_MONTH_OR_DAY = 7;
+const int PARSER_INVALID_MONTH = 8;
+const int PARSER_INVALID_DAY_FOR_MONTH = 9;
+const int PARSER_INVALID_HOUR = 10;
+const int PARSER_INVALID_MINUTE = 11;
+const int PARSER_INVALID_SECOND = 12;
+const int PARSER_INVALID_SUBSECOND = 13;
+const int PARSER_INVALID_TZ_OFFSET = 14;
+const int PARSER_INVALID_DURATION = 15;
+const int PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED = 16;
+
+const char PARSER_ERRORS[17][80] = {
+ "Invalid ISO 8601 string",
+ "Invalid date",
+ "Invalid time",
+ "Invalid week date",
+ "Invalid week number",
+ "Invalid weekday number",
+ "Invalid ordinal day for year",
+ "Invalid month and/or day",
+ "Invalid month",
+ "Invalid day for month",
+ "Invalid hour",
+ "Invalid minute",
+ "Invalid second",
+ "Invalid subsecond",
+ "Invalid timezone offset",
+ "Invalid duration",
+ "Float years and months are not supported"
+};
+
+/* ------------------------------------------------------------------------- */
+
+
+int p(int y) {
+ return y + y/4 - y/100 + y/400;
+}
+
+int is_leap(int year) {
+ return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
+}
+
+int week_day(int year, int month, int day) {
+ int y;
+ int w;
+
+ y = year - (month < 3);
+
+ w = (p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7;
+
+ if (!w) {
+ w = 7;
+ }
+
+ return w;
+}
+
+int days_in_year(int year) {
+ if (is_leap(year)) {
+ return DAYS_PER_L_YEAR;
+ }
+
+ return DAYS_PER_N_YEAR;
+}
+
+int is_long_year(int year) {
+ return (p(year) % 7 == 4) || (p(year - 1) % 7 == 3);
+}
+
+
+/* ------------------------ Custom Types ------------------------------- */
+
+
+/*
+ * class FixedOffset(tzinfo):
+ */
+typedef struct {
+ PyObject_HEAD
+ int offset;
+ char *tzname;
+} FixedOffset;
+
+/*
+ * def __init__(self, offset):
+ * self.offset = offset
+*/
+static int FixedOffset_init(FixedOffset *self, PyObject *args, PyObject *kwargs) {
+ int offset;
+ char *tzname = NULL;
+
+ static char *kwlist[] = {"offset", "tzname", NULL};
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|s", kwlist, &offset, &tzname))
+ return -1;
+
+ self->offset = offset;
+ self->tzname = tzname;
+
+ return 0;
+}
+
+/*
+ * def utcoffset(self, dt):
+ * return timedelta(seconds=self.offset * 60)
+ */
+static PyObject *FixedOffset_utcoffset(FixedOffset *self, PyObject *args) {
+ return PyDelta_FromDSU(0, self->offset, 0);
+}
+
+/*
+ * def dst(self, dt):
+ * return timedelta(seconds=self.offset * 60)
+ */
+static PyObject *FixedOffset_dst(FixedOffset *self, PyObject *args) {
+ return PyDelta_FromDSU(0, self->offset, 0);
+}
+
+/*
+ * def tzname(self, dt):
+ * sign = '+'
+ * if self.offset < 0:
+ * sign = '-'
+ * return "%s%d:%d" % (sign, self.offset / 60, self.offset % 60)
+ */
+static PyObject *FixedOffset_tzname(FixedOffset *self, PyObject *args) {
+ if (self->tzname != NULL) {
+ return PyUnicode_FromString(self->tzname);
+ }
+
+ char tzname_[7] = {0};
+ char sign = '+';
+ int offset = self->offset;
+
+ if (offset < 0) {
+ sign = '-';
+ offset *= -1;
+ }
+
+ sprintf(
+ tzname_,
+ "%c%02d:%02d",
+ sign,
+ offset / SECS_PER_HOUR,
+ offset / SECS_PER_MIN % SECS_PER_MIN
+ );
+
+ return PyUnicode_FromString(tzname_);
+}
+
+/*
+ * def __repr__(self):
+ * return self.tzname()
+ */
+static PyObject *FixedOffset_repr(FixedOffset *self) {
+ return FixedOffset_tzname(self, NULL);
+}
+
+/*
+ * Class member / class attributes
+ */
+static PyMemberDef FixedOffset_members[] = {
+ {"offset", T_INT, offsetof(FixedOffset, offset), 0, "UTC offset"},
+ {NULL}
+};
+
+/*
+ * Class methods
+ */
+static PyMethodDef FixedOffset_methods[] = {
+ {"utcoffset", (PyCFunction)FixedOffset_utcoffset, METH_VARARGS, ""},
+ {"dst", (PyCFunction)FixedOffset_dst, METH_VARARGS, ""},
+ {"tzname", (PyCFunction)FixedOffset_tzname, METH_VARARGS, ""},
+ {NULL}
+};
+
+static PyTypeObject FixedOffset_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "FixedOffset_type", /* tp_name */
+ sizeof(FixedOffset), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ 0, /* tp_dealloc */
+ 0, /* tp_print */
+ 0, /* tp_getattr */
+ 0, /* tp_setattr */
+ 0, /* tp_as_async */
+ (reprfunc)FixedOffset_repr, /* tp_repr */
+ 0, /* tp_as_number */
+ 0, /* tp_as_sequence */
+ 0, /* tp_as_mapping */
+ 0, /* tp_hash */
+ 0, /* tp_call */
+ (reprfunc)FixedOffset_repr, /* tp_str */
+ 0, /* tp_getattro */
+ 0, /* tp_setattro */
+ 0, /* tp_as_buffer */
+ Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */
+ "TZInfo with fixed offset", /* tp_doc */
+};
+
+/*
+ * Instantiate new FixedOffset_type object
+ * Skip overhead of calling PyObject_New and PyObject_Init.
+ * Directly allocate object.
+ */
+static PyObject *new_fixed_offset_ex(int offset, char *name, PyTypeObject *type) {
+ FixedOffset *self = (FixedOffset *) (type->tp_alloc(type, 0));
+
+ if (self != NULL)
+ self->offset = offset;
+ self->tzname = name;
+
+ return (PyObject *) self;
+}
+
+#define new_fixed_offset(offset, name) new_fixed_offset_ex(offset, name, &FixedOffset_type)
+
+
+/*
+ * class Duration():
+ */
+typedef struct {
+ PyObject_HEAD
+ int years;
+ int months;
+ int weeks;
+ int days;
+ int hours;
+ int minutes;
+ int seconds;
+ int microseconds;
+} Duration;
+
+/*
+ * def __init__(self, years, months, days, hours, minutes, seconds, microseconds):
+ * self.years = years
+ * # ...
+*/
+static int Duration_init(Duration *self, PyObject *args, PyObject *kwargs) {
+ int years;
+ int months;
+ int weeks;
+ int days;
+ int hours;
+ int minutes;
+ int seconds;
+ int microseconds;
+
+ if (!PyArg_ParseTuple(args, "iiiiiiii", &years, &months, &weeks, &days, &hours, &minutes, &seconds, &microseconds))
+ return -1;
+
+ self->years = years;
+ self->months = months;
+ self->weeks = weeks;
+ self->days = days;
+ self->hours = hours;
+ self->minutes = minutes;
+ self->seconds = seconds;
+ self->microseconds = microseconds;
+
+ return 0;
+}
+
+/*
+ * def __repr__(self):
+ * return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format(
+ * self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds
+ * )
+ */
+static PyObject *Duration_repr(Duration *self) {
+ char repr[82] = {0};
+
+ sprintf(
+ repr,
+ "%d years %d months %d weeks %d days %d hours %d minutes %d seconds %d microseconds",
+ self->years,
+ self->months,
+ self->weeks,
+ self->days,
+ self->hours,
+ self->minutes,
+ self->seconds,
+ self->microseconds
+ );
+
+ return PyUnicode_FromString(repr);
+}
+
+/*
+ * Instantiate new Duration_type object
+ * Skip overhead of calling PyObject_New and PyObject_Init.
+ * Directly allocate object.
+ */
+static PyObject *new_duration_ex(int years, int months, int weeks, int days, int hours, int minutes, int seconds, int microseconds, PyTypeObject *type) {
+ Duration *self = (Duration *) (type->tp_alloc(type, 0));
+
+ if (self != NULL) {
+ self->years = years;
+ self->months = months;
+ self->weeks = weeks;
+ self->days = days;
+ self->hours = hours;
+ self->minutes = minutes;
+ self->seconds = seconds;
+ self->microseconds = microseconds;
+ }
+
+ return (PyObject *) self;
+}
+
+/*
+ * Class member / class attributes
+ */
+static PyMemberDef Duration_members[] = {
+ {"years", T_INT, offsetof(Duration, years), 0, "years in duration"},
+ {"months", T_INT, offsetof(Duration, months), 0, "months in duration"},
+ {"weeks", T_INT, offsetof(Duration, weeks), 0, "weeks in duration"},
+ {"days", T_INT, offsetof(Duration, days), 0, "days in duration"},
+ {"remaining_days", T_INT, offsetof(Duration, days), 0, "days in duration"},
+ {"hours", T_INT, offsetof(Duration, hours), 0, "hours in duration"},
+ {"minutes", T_INT, offsetof(Duration, minutes), 0, "minutes in duration"},
+ {"seconds", T_INT, offsetof(Duration, seconds), 0, "seconds in duration"},
+ {"remaining_seconds", T_INT, offsetof(Duration, seconds), 0, "seconds in duration"},
+ {"microseconds", T_INT, offsetof(Duration, microseconds), 0, "microseconds in duration"},
+ {NULL}
+};
+
+static PyTypeObject Duration_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "Duration", /* tp_name */
+ sizeof(Duration), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ 0, /* tp_dealloc */
+ 0, /* tp_print */
+ 0, /* tp_getattr */
+ 0, /* tp_setattr */
+ 0, /* tp_as_async */
+ (reprfunc)Duration_repr, /* tp_repr */
+ 0, /* tp_as_number */
+ 0, /* tp_as_sequence */
+ 0, /* tp_as_mapping */
+ 0, /* tp_hash */
+ 0, /* tp_call */
+ (reprfunc)Duration_repr, /* tp_str */
+ 0, /* tp_getattro */
+ 0, /* tp_setattro */
+ 0, /* tp_as_buffer */
+ Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */
+ "Duration", /* tp_doc */
+};
+
+#define new_duration(years, months, weeks, days, hours, minutes, seconds, microseconds) new_duration_ex(years, months, weeks, days, hours, minutes, seconds, microseconds, &Duration_type)
+
+typedef struct {
+ int is_date;
+ int is_time;
+ int is_datetime;
+ int is_duration;
+ int is_period;
+ int ambiguous;
+ int year;
+ int month;
+ int day;
+ int hour;
+ int minute;
+ int second;
+ int microsecond;
+ int offset;
+ int has_offset;
+ char *tzname;
+ int years;
+ int months;
+ int weeks;
+ int days;
+ int hours;
+ int minutes;
+ int seconds;
+ int microseconds;
+ int error;
+} Parsed;
+
+
+Parsed* new_parsed() {
+ Parsed *parsed;
+
+ if((parsed = malloc(sizeof *parsed)) != NULL) {
+ parsed->is_date = 0;
+ parsed->is_time = 0;
+ parsed->is_datetime = 0;
+ parsed->is_duration = 0;
+ parsed->is_period = 0;
+
+ parsed->ambiguous = 0;
+ parsed->year = 0;
+ parsed->month = 1;
+ parsed->day = 1;
+ parsed->hour = 0;
+ parsed->minute = 0;
+ parsed->second = 0;
+ parsed->microsecond = 0;
+ parsed->offset = 0;
+ parsed->has_offset = 0;
+ parsed->tzname = NULL;
+
+ parsed->years = 0;
+ parsed->months = 0;
+ parsed->weeks = 0;
+ parsed->days = 0;
+ parsed->hours = 0;
+ parsed->minutes = 0;
+ parsed->seconds = 0;
+ parsed->microseconds = 0;
+
+ parsed->error = -1;
+ }
+
+ return parsed;
+}
+
+
+/* -------------------------- Functions --------------------------*/
+
+Parsed* _parse_iso8601_datetime(char *str, Parsed *parsed) {
+ char* c;
+ int monthday = 0;
+ int week = 0;
+ int weekday = 1;
+ int ordinal;
+ int tz_sign = 0;
+ int leap = 0;
+ int separators = 0;
+ int time = 0;
+ int has_hour = 0;
+ int i;
+ int j;
+
+ // Assuming date only for now
+ parsed->is_date = 1;
+
+ c = str;
+
+ for (i = 0; i < 4; i++) {
+ if (*c >= '0' && *c <= '9') {
+ parsed->year = 10 * parsed->year + *c++ - '0';
+ } else {
+ parsed->error = PARSER_INVALID_ISO8601;
+
+ return NULL;
+ }
+ }
+
+ leap = is_leap(parsed->year);
+
+ // Optional separator
+ if (*c == '-') {
+ separators++;
+ c++;
+ }
+
+ // Checking for week dates
+ if (*c == 'W') {
+ c++;
+
+ i = 0;
+ while (*c != '\0' && *c != ' ' && *c != 'T') {
+ if (*c == '-') {
+ separators++;
+ c++;
+ continue;
+ }
+
+ week = 10 * week + *c++ - '0';
+
+ i++;
+ }
+
+ switch (i) {
+ case 2:
+ // Only week number
+ break;
+ case 3:
+ // Week with weekday
+ if (!(separators == 0 || separators == 2)) {
+ // We should have 2 or no separator
+ parsed->error = PARSER_INVALID_WEEK_DATE;
+
+ return NULL;
+ }
+
+ weekday = week % 10;
+ week /= 10;
+
+ break;
+ default:
+ // Any other case is wrong
+ parsed->error = PARSER_INVALID_WEEK_DATE;
+
+ return NULL;
+ }
+
+ // Checks
+ if (week > 53 || (week > 52 && !is_long_year(parsed->year))) {
+ parsed->error = PARSER_INVALID_WEEK_NUMBER;
+
+ return NULL;
+ }
+
+ if (weekday > 7) {
+ parsed->error = PARSER_INVALID_WEEKDAY_NUMBER;
+
+ return NULL;
+ }
+
+ // Calculating ordinal day
+ ordinal = week * 7 + weekday - (week_day(parsed->year, 1, 4) + 3);
+
+ if (ordinal < 1) {
+ // Previous year
+ ordinal += days_in_year(parsed->year - 1);
+ parsed->year -= 1;
+ leap = is_leap(parsed->year);
+ }
+
+ if (ordinal > days_in_year(parsed->year)) {
+ // Next year
+ ordinal -= days_in_year(parsed->year);
+ parsed->year += 1;
+ leap = is_leap(parsed->year);
+ }
+
+ for (j = 1; j < 14; j++) {
+ if (ordinal <= MONTHS_OFFSETS[leap][j]) {
+ parsed->day = ordinal - MONTHS_OFFSETS[leap][j - 1];
+ parsed->month = j - 1;
+
+ break;
+ }
+ }
+ } else {
+ // At this point we need to check the number
+ // of characters until the end of the date part
+ // (or the end of the string).
+ //
+ // If two, we have only a month if there is a separator, it may be a time otherwise.
+ // If three, we have an ordinal date.
+ // If four, we have a complete date
+ i = 0;
+ while (*c != '\0' && *c != ' ' && *c != 'T') {
+ if (*c == '-') {
+ separators++;
+ c++;
+ continue;
+ }
+
+ if (!(*c >= '0' && *c <='9')) {
+ parsed->error = PARSER_INVALID_DATE;
+
+ return NULL;
+ }
+
+ monthday = 10 * monthday + *c++ - '0';
+
+ i++;
+ }
+
+ switch (i) {
+ case 0:
+ // No month/day specified (only a year)
+ break;
+ case 2:
+ if (!separators) {
+ // The date looks like 201207
+ // which is invalid for a date
+ // But it might be a time in the form hhmmss
+ parsed->ambiguous = 1;
+ } else if (separators > 1) {
+ parsed->error = PARSER_INVALID_DATE;
+
+ return NULL;
+ }
+
+ parsed->month = monthday;
+ break;
+ case 3:
+ // Ordinal day
+ if (separators > 1) {
+ parsed->error = PARSER_INVALID_DATE;
+
+ return NULL;
+ }
+
+ if (monthday < 1 || monthday > MONTHS_OFFSETS[leap][13]) {
+ parsed->error = PARSER_INVALID_ORDINAL_DAY_FOR_YEAR;
+
+ return NULL;
+ }
+
+ for (j = 1; j < 14; j++) {
+ if (monthday <= MONTHS_OFFSETS[leap][j]) {
+ parsed->day = monthday - MONTHS_OFFSETS[leap][j - 1];
+ parsed->month = j - 1;
+
+ break;
+ }
+ }
+
+ break;
+ case 4:
+ // Month and day
+ parsed->month = monthday / 100;
+ parsed->day = monthday % 100;
+
+ break;
+ default:
+ parsed->error = PARSER_INVALID_MONTH_OR_DAY;
+
+ return NULL;
+ }
+ }
+
+ // Checks
+ if (separators && !monthday && !week) {
+ parsed->error = PARSER_INVALID_DATE;
+
+ return NULL;
+ }
+
+ if (parsed->month > 12) {
+ parsed->error = PARSER_INVALID_MONTH;
+
+ return NULL;
+ }
+
+ if (parsed->day > DAYS_PER_MONTHS[leap][parsed->month]) {
+ parsed->error = PARSER_INVALID_DAY_FOR_MONTH;
+
+ return NULL;
+ }
+
+ separators = 0;
+ if (*c == 'T' || *c == ' ') {
+ if (parsed->ambiguous) {
+ parsed->error = PARSER_INVALID_DATE;
+
+ return NULL;
+ }
+
+ // We have time so we have a datetime
+ parsed->is_datetime = 1;
+ parsed->is_date = 0;
+
+ c++;
+
+ // Grabbing time information
+ i = 0;
+ while (*c != '\0' && *c != '.' && *c != ',' && *c != 'Z' && *c != '+' && *c != '-') {
+ if (*c == ':') {
+ separators++;
+ c++;
+ continue;
+ }
+
+ if (!(*c >= '0' && *c <='9')) {
+ parsed->error = PARSER_INVALID_TIME;
+
+ return NULL;
+ }
+
+ time = 10 * time + *c++ - '0';
+ i++;
+ }
+
+ switch (i) {
+ case 2:
+ // Hours only
+ if (separators > 0) {
+ // Extraneous separators
+ parsed->error = PARSER_INVALID_TIME;
+
+ return NULL;
+ }
+
+ parsed->hour = time;
+ has_hour = 1;
+ break;
+ case 4:
+ // Hours and minutes
+ if (separators > 1) {
+ // Extraneous separators
+ parsed->error = PARSER_INVALID_TIME;
+
+ return NULL;
+ }
+
+ parsed->hour = time / 100;
+ parsed->minute = time % 100;
+ has_hour = 1;
+ break;
+ case 6:
+ // Hours, minutes and seconds
+ if (!(separators == 0 || separators == 2)) {
+ // We should have either two separators or none
+ parsed->error = PARSER_INVALID_TIME;
+
+ return NULL;
+ }
+
+ parsed->hour = time / 10000;
+ parsed->minute = time / 100 % 100;
+ parsed->second = time % 100;
+ has_hour = 1;
+ break;
+ default:
+ // Any other case is wrong
+ parsed->error = PARSER_INVALID_TIME;
+
+ return NULL;
+ }
+
+ // Checks
+ if (parsed->hour > 23) {
+ parsed->error = PARSER_INVALID_HOUR;
+
+ return NULL;
+ }
+
+ if (parsed->minute > 59) {
+ parsed->error = PARSER_INVALID_MINUTE;
+
+ return NULL;
+ }
+
+ if (parsed->second > 59) {
+ parsed->error = PARSER_INVALID_SECOND;
+
+ return NULL;
+ }
+
+ // Subsecond
+ if (*c == '.' || *c == ',') {
+ c++;
+
+ time = 0;
+ i = 0;
+ while (*c != '\0' && *c != 'Z' && *c != '+' && *c != '-') {
+ if (!(*c >= '0' && *c <='9')) {
+ parsed->error = PARSER_INVALID_SUBSECOND;
+
+ return NULL;
+ }
+
+ time = 10 * time + *c++ - '0';
+ i++;
+ }
+
+ // adjust to microseconds
+ if (i > 6) {
+ parsed->microsecond = time / pow(10, i - 6);
+ } else if (i <= 6) {
+ parsed->microsecond = time * pow(10, 6 - i);
+ }
+ }
+
+ // Timezone
+ if (*c == 'Z') {
+ parsed->has_offset = 1;
+ parsed->tzname = "UTC";
+ c++;
+ } else if (*c == '+' || *c == '-') {
+ tz_sign = 1;
+ if (*c == '-') {
+ tz_sign = -1;
+ }
+
+ parsed->has_offset = 1;
+ c++;
+
+ i = 0;
+ time = 0;
+ separators = 0;
+ while (*c != '\0') {
+ if (*c == ':') {
+ separators++;
+ c++;
+ continue;
+ }
+
+ if (!(*c >= '0' && *c <= '9')) {
+ parsed->error = PARSER_INVALID_TZ_OFFSET;
+
+ return NULL;
+ }
+
+ time = 10 * time + *c++ - '0';
+ i++;
+ }
+
+ switch (i) {
+ case 2:
+ // hh Format
+ if (separators) {
+ // Extraneous separators
+ parsed->error = PARSER_INVALID_TZ_OFFSET;
+
+ return NULL;
+ }
+
+ parsed->offset = tz_sign * (time * 3600);
+ break;
+ case 4:
+ // hhmm Format
+ if (separators > 1) {
+ // Extraneous separators
+ parsed->error = PARSER_INVALID_TZ_OFFSET;
+
+ return NULL;
+ }
+
+ parsed->offset = tz_sign * ((time / 100 * 3600) + (time % 100 * 60));
+ break;
+ default:
+ // Wrong format
+ parsed->error = PARSER_INVALID_TZ_OFFSET;
+
+ return NULL;
+ }
+ }
+ }
+
+ // At this point we should be at the end of the string
+ // If not, the string is invalid
+ if (*c != '\0') {
+ parsed->error = PARSER_INVALID_ISO8601;
+
+ return NULL;
+ }
+
+ return parsed;
+}
+
+
+Parsed* _parse_iso8601_duration(char *str, Parsed *parsed) {
+ char* c;
+ int value = 0;
+ int grabbed = 0;
+ int in_time = 0;
+ int in_fraction = 0;
+ int fraction_length = 0;
+ int has_fractional = 0;
+ int fraction = 0;
+ int has_ymd = 0;
+ int has_week = 0;
+ int has_year = 0;
+ int has_month = 0;
+ int has_day = 0;
+ int has_hour = 0;
+ int has_minute = 0;
+ int has_second = 0;
+
+ c = str;
+
+ // Removing P operator
+ c++;
+
+ parsed->is_duration = 1;
+
+ for (; *c != '\0'; c++) {
+ switch (*c) {
+ case 'Y':
+ if (!grabbed || in_time || has_week || has_ymd) {
+ // No value grabbed
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (fraction) {
+ parsed->error = PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED;
+
+ return NULL;
+ }
+
+ parsed->years = value;
+
+ grabbed = 0;
+ value = 0;
+ fraction = 0;
+ in_fraction = 0;
+ has_ymd = 1;
+ has_year = 1;
+
+ break;
+ case 'M':
+ if (!grabbed || has_week) {
+ // No value grabbed
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (in_time) {
+ if (has_second) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (has_fractional) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ parsed->minutes = value;
+ if (fraction) {
+ parsed->seconds = fraction * 6;
+ has_fractional = 1;
+ }
+
+ has_minute = 1;
+ } else {
+ if (fraction) {
+ parsed->error = PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED;
+
+ return NULL;
+ }
+
+ if (has_month || has_day) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ parsed->months = value;
+ has_ymd = 1;
+ has_month = 1;
+ }
+
+ grabbed = 0;
+ value = 0;
+ fraction = 0;
+ in_fraction = 0;
+
+ break;
+ case 'D':
+ if (!grabbed || in_time || has_week) {
+ // No value grabbed
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (has_day) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ parsed->days = value;
+ if (fraction) {
+ parsed->hours = fraction * 2.4;
+ has_fractional = 1;
+ }
+
+ grabbed = 0;
+ value = 0;
+ fraction = 0;
+ in_fraction = 0;
+ has_ymd = 1;
+ has_day = 1;
+
+ break;
+ case 'T':
+ if (grabbed) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ in_time = 1;
+
+ break;
+ case 'H':
+ if (!grabbed || !in_time || has_week) {
+ // No value grabbed
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (has_hour || has_second || has_minute) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (has_fractional) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ parsed->hours = value;
+ if (fraction) {
+ parsed->minutes = fraction * 6;
+ has_fractional = 1;
+ }
+
+ grabbed = 0;
+ value = 0;
+ fraction = 0;
+ in_fraction = 0;
+ has_hour = 1;
+
+ break;
+ case 'S':
+ if (!grabbed || !in_time || has_week) {
+ // No value grabbed
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (has_second) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (has_fractional) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (fraction) {
+ parsed->seconds = value;
+ if (fraction_length > 6) {
+ parsed->microseconds = fraction / pow(10, fraction_length - 6);
+ } else {
+ parsed->microseconds = fraction * pow(10, 6 - fraction_length);
+ }
+ has_fractional = 1;
+ } else {
+ parsed->seconds = value;
+ }
+
+ grabbed = 0;
+ value = 0;
+ fraction = 0;
+ in_fraction = 0;
+ has_second = 1;
+
+ break;
+ case 'W':
+ if (!grabbed || in_time || has_ymd) {
+ // No value grabbed
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ parsed->weeks = value;
+ if (fraction) {
+ float days;
+ days = fraction * 0.7;
+ parsed->hours = (int) ((days - (int) days) * 24);
+ parsed->days = (int) days;
+ }
+
+ grabbed = 0;
+ value = 0;
+ fraction = 0;
+ in_fraction = 0;
+ has_week = 1;
+
+ break;
+ case '.':
+ if (!grabbed || has_fractional) {
+ // No value grabbed
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ in_fraction = 1;
+
+ break;
+ case ',':
+ if (!grabbed || has_fractional) {
+ // No value grabbed
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ in_fraction = 1;
+
+ break;
+ default:
+ if (*c >= '0' && *c <='9') {
+ if (in_fraction) {
+ fraction = 10 * fraction + *c - '0';
+ fraction_length++;
+ } else {
+ value = 10 * value + *c - '0';
+ grabbed = 1;
+ }
+ break;
+ }
+
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+ }
+
+ return parsed;
+}
+
+
+PyObject* parse_iso8601(PyObject *self, PyObject *args) {
+ char* str;
+ PyObject *obj;
+ PyObject *tzinfo;
+ Parsed *parsed = new_parsed();
+
+ if (!PyArg_ParseTuple(args, "s", &str)) {
+ PyErr_SetString(
+ PyExc_ValueError, "Invalid parameters"
+ );
+ return NULL;
+ }
+
+ if (*str == 'P') {
+ // Duration (or interval)
+ if (_parse_iso8601_duration(str, parsed) == NULL) {
+ PyErr_SetString(
+ PyExc_ValueError, PARSER_ERRORS[parsed->error]
+ );
+
+ return NULL;
+ }
+ } else if (_parse_iso8601_datetime(str, parsed) == NULL) {
+ PyErr_SetString(
+ PyExc_ValueError, PARSER_ERRORS[parsed->error]
+ );
+
+ return NULL;
+ }
+
+ if (parsed->is_date) {
+ // Date only
+ if (parsed->ambiguous) {
+ // We can "safely" assume that the ambiguous
+ // date was actually a time in the form hhmmss
+ parsed->hour = parsed->year / 100;
+ parsed->minute = parsed->year % 100;
+ parsed->second = parsed->month;
+
+ obj = PyDateTimeAPI->Time_FromTime(
+ parsed->hour, parsed->minute, parsed->second, parsed->microsecond,
+ Py_BuildValue(""),
+ PyDateTimeAPI->TimeType
+ );
+ } else {
+ obj = PyDateTimeAPI->Date_FromDate(
+ parsed->year, parsed->month, parsed->day,
+ PyDateTimeAPI->DateType
+ );
+ }
+ } else if (parsed->is_datetime) {
+ if (!parsed->has_offset) {
+ tzinfo = Py_BuildValue("");
+ } else {
+ tzinfo = new_fixed_offset(parsed->offset, parsed->tzname);
+ }
+
+ obj = PyDateTimeAPI->DateTime_FromDateAndTime(
+ parsed->year,
+ parsed->month,
+ parsed->day,
+ parsed->hour,
+ parsed->minute,
+ parsed->second,
+ parsed->microsecond,
+ tzinfo,
+ PyDateTimeAPI->DateTimeType
+ );
+
+ Py_DECREF(tzinfo);
+ } else if (parsed->is_duration) {
+ obj = new_duration(
+ parsed->years, parsed->months, parsed->weeks, parsed->days,
+ parsed->hours, parsed->minutes, parsed->seconds, parsed->microseconds
+ );
+ } else {
+ return NULL;
+ }
+
+ free(parsed);
+
+ return obj;
+}
+
+
+/* ------------------------------------------------------------------------- */
+
+static PyMethodDef helpers_methods[] = {
+ {
+ "parse_iso8601",
+ (PyCFunction) parse_iso8601,
+ METH_VARARGS,
+ PyDoc_STR("Parses a ISO8601 string into a tuple.")
+ },
+ {NULL}
+};
+
+
+/* ------------------------------------------------------------------------- */
+
+static struct PyModuleDef moduledef = {
+ PyModuleDef_HEAD_INIT,
+ "_iso8601",
+ NULL,
+ -1,
+ helpers_methods,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+};
+
+PyMODINIT_FUNC
+PyInit__iso8601(void)
+{
+ PyObject *module;
+
+ PyDateTime_IMPORT;
+
+ module = PyModule_Create(&moduledef);
+
+ if (module == NULL)
+ return NULL;
+
+ // FixedOffset declaration
+ FixedOffset_type.tp_new = PyType_GenericNew;
+ FixedOffset_type.tp_base = PyDateTimeAPI->TZInfoType;
+ FixedOffset_type.tp_methods = FixedOffset_methods;
+ FixedOffset_type.tp_members = FixedOffset_members;
+ FixedOffset_type.tp_init = (initproc)FixedOffset_init;
+
+ if (PyType_Ready(&FixedOffset_type) < 0)
+ return NULL;
+
+ // Duration declaration
+ Duration_type.tp_new = PyType_GenericNew;
+ Duration_type.tp_members = Duration_members;
+ Duration_type.tp_init = (initproc)Duration_init;
+
+ if (PyType_Ready(&Duration_type) < 0)
+ return NULL;
+
+ Py_INCREF(&FixedOffset_type);
+ Py_INCREF(&Duration_type);
+
+ PyModule_AddObject(module, "TZFixedOffset", (PyObject *)&FixedOffset_type);
+ PyModule_AddObject(module, "Duration", (PyObject *)&Duration_type);
+
+ return module;
+}
diff --git a/pendulum/parsing/exceptions/__init__.py b/pendulum/parsing/exceptions/__init__.py
new file mode 100644
index 0000000..997b0fa
--- /dev/null
+++ b/pendulum/parsing/exceptions/__init__.py
@@ -0,0 +1,3 @@
+class ParserError(ValueError):
+
+ pass
diff --git a/pendulum/parsing/iso8601.py b/pendulum/parsing/iso8601.py
new file mode 100644
index 0000000..40efa2f
--- /dev/null
+++ b/pendulum/parsing/iso8601.py
@@ -0,0 +1,447 @@
+from __future__ import division
+
+import datetime
+import re
+
+from ..constants import HOURS_PER_DAY
+from ..constants import MINUTES_PER_HOUR
+from ..constants import MONTHS_OFFSETS
+from ..constants import SECONDS_PER_MINUTE
+from ..duration import Duration
+from ..helpers import days_in_year
+from ..helpers import is_leap
+from ..helpers import is_long_year
+from ..helpers import week_day
+from ..tz.timezone import UTC
+from ..tz.timezone import FixedTimezone
+from .exceptions import ParserError
+
+
+ISO8601_DT = re.compile(
+ # Date (optional)
+ "^"
+ "(?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)
+ "(?P<time>"
+ r" (?P<timesep>[T\ ])?" # Separator (T or space)
+ r" (?P<hour>\d{1,2})(?P<minsep>:)?(?P<minute>\d{1,2})?(?P<secsep>:)?(?P<second>\d{1,2})?" # HH:mm:ss (optional mm and ss)
+ # 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)
+ "(?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):
+ """
+ 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 = None
+
+ if m:
+ 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("Invalid date string: {}".format(text))
+
+ if not m.group("weeksep") and m.group("weekdaysep"):
+ raise ParserError("Invalid date string: {}".format(text))
+
+ try:
+ date = _get_iso_8601_week(
+ m.group("isoyear"), m.group("isoweek"), m.group("isoweekday")
+ )
+ except ParserError:
+ raise
+ except ValueError:
+ raise ParserError("Invalid date string: {}".format(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 = "{}{:0>2}".format(str(year), str(month))
+
+ return datetime.time(int(hhmmss[:2]), int(hhmmss[2:4]), int(hhmmss[4:]))
+
+ return datetime.date(year, month, day)
+
+ if ambiguous_date:
+ raise ParserError("Invalid date string: {}".format(text))
+
+ if is_date and not m.group("timesep"):
+ raise ParserError("Invalid date string: {}".format(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("{:0<6}".format(subsecond))
+
+ # Grabbing timezone, if any
+ tz = m.group("tz")
+ if tz:
+ if tz == "Z":
+ tzinfo = UTC
+ else:
+ negative = True if tz.startswith("-") else False
+ tz = tz[1:]
+ if ":" not in tz:
+ if len(tz) == 2:
+ tz = "{}00".format(tz)
+
+ 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)
+
+ return datetime.datetime(
+ year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo
+ )
+
+
+def _parse_iso8601_duration(text, **options):
+ m = ISO8601_DURATION.match(text)
+ if not m:
+ return
+
+ years = 0
+ months = 0
+ weeks = 0
+ days = 0
+ hours = 0
+ minutes = 0
+ seconds = 0
+ microseconds = 0
+ fractional = False
+
+ 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), _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 = _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 = _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 = _seconds.replace(",", ".").replace("S", "")
+
+ if "." in _seconds:
+ _seconds, _microseconds = _seconds.split(".")
+ seconds += int(_seconds)
+ microseconds += int("{:0<6}".format(_microseconds[: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, week, weekday):
+ if not weekday:
+ weekday = 1
+ else:
+ weekday = 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 = "{}-{}".format(year, ordinal)
+
+ dt = datetime.datetime.strptime(string, fmt)
+
+ return {"year": dt.year, "month": dt.month, "day": dt.day}
diff --git a/pendulum/period.py b/pendulum/period.py
new file mode 100644
index 0000000..b734b5b
--- /dev/null
+++ b/pendulum/period.py
@@ -0,0 +1,390 @@
+from __future__ import absolute_import
+
+import operator
+
+from datetime import date
+from datetime import datetime
+from datetime import timedelta
+
+import pendulum
+
+from pendulum.utils._compat import _HAS_FOLD
+from pendulum.utils._compat import decode
+
+from .constants import MONTHS_PER_YEAR
+from .duration import Duration
+from .helpers import precise_diff
+
+
+class Period(Duration):
+ """
+ Duration class that is aware of the datetimes that generated the
+ time difference.
+ """
+
+ def __new__(cls, start, end, absolute=False):
+ if isinstance(start, datetime) and isinstance(end, datetime):
+ if (
+ 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):
+ if _HAS_FOLD:
+ _start = datetime(
+ start.year,
+ start.month,
+ start.day,
+ start.hour,
+ start.minute,
+ start.second,
+ start.microsecond,
+ tzinfo=start.tzinfo,
+ fold=start.fold,
+ )
+ else:
+ _start = datetime(
+ start.year,
+ start.month,
+ start.day,
+ start.hour,
+ start.minute,
+ start.second,
+ start.microsecond,
+ tzinfo=start.tzinfo,
+ )
+ elif isinstance(start, pendulum.Date):
+ _start = date(start.year, start.month, start.day)
+
+ if isinstance(end, pendulum.DateTime):
+ if _HAS_FOLD:
+ _end = datetime(
+ end.year,
+ end.month,
+ end.day,
+ end.hour,
+ end.minute,
+ end.second,
+ end.microsecond,
+ tzinfo=end.tzinfo,
+ fold=end.fold,
+ )
+ else:
+ _end = datetime(
+ end.year,
+ end.month,
+ end.day,
+ end.hour,
+ end.minute,
+ end.second,
+ end.microsecond,
+ tzinfo=end.tzinfo,
+ )
+ 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:
+ _start = (_start - start.utcoffset()).replace(tzinfo=None)
+
+ if isinstance(end, datetime) and _end.tzinfo is not None:
+ _end = (_end - end.utcoffset()).replace(tzinfo=None)
+
+ delta = _end - _start
+
+ return super(Period, cls).__new__(cls, seconds=delta.total_seconds())
+
+ def __init__(self, start, end, absolute=False):
+ super(Period, self).__init__()
+
+ 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)
+
+ 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 = start
+ self._end = end
+ self._delta = precise_diff(_start, _end)
+
+ @property
+ def years(self):
+ return self._delta.years
+
+ @property
+ def months(self):
+ return self._delta.months
+
+ @property
+ def weeks(self):
+ return abs(self._delta.days) // 7 * self._sign(self._delta.days)
+
+ @property
+ def days(self):
+ return self._days
+
+ @property
+ def remaining_days(self):
+ return abs(self._delta.days) % 7 * self._sign(self._days)
+
+ @property
+ def hours(self):
+ return self._delta.hours
+
+ @property
+ def minutes(self):
+ return self._delta.minutes
+
+ @property
+ def start(self):
+ return self._start
+
+ @property
+ def end(self):
+ return self._end
+
+ def in_years(self):
+ """
+ Gives the duration of the Period in full years.
+
+ :rtype: int
+ """
+ return self.years
+
+ def in_months(self):
+ """
+ Gives the duration of the Period in full months.
+
+ :rtype: int
+ """
+ return self.years * MONTHS_PER_YEAR + self.months
+
+ def in_weeks(self):
+ days = self.in_days()
+ sign = 1
+
+ if days < 0:
+ sign = -1
+
+ return sign * (abs(days) // 7)
+
+ def in_days(self):
+ return self._delta.total_days
+
+ def in_words(self, locale=None, separator=" "):
+ """
+ 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.
+ :type locale: str
+
+ :param separator: The separator to use between each unit
+ :type separator: str
+
+ :rtype: str
+ """
+ periods = [
+ ("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()
+
+ locale = pendulum.locale(locale)
+ parts = []
+ for period in periods:
+ unit, count = period
+ if abs(count) > 0:
+ translation = locale.translation(
+ "units.{}.{}".format(unit, locale.plural(abs(count)))
+ )
+ parts.append(translation.format(count))
+
+ if not parts:
+ if abs(self.microseconds) > 0:
+ unit = "units.second.{}".format(locale.plural(1))
+ count = "{:.2f}".format(abs(self.microseconds) / 1e6)
+ else:
+ unit = "units.microsecond.{}".format(locale.plural(0))
+ count = 0
+ translation = locale.translation(unit)
+ parts.append(translation.format(count))
+
+ return decode(separator.join(parts))
+
+ def range(self, unit, amount=1):
+ 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 start
+
+ start = getattr(self.start, method)(**{unit: i})
+
+ i += amount
+
+ def as_interval(self):
+ """
+ Return the Period as an Duration.
+
+ :rtype: Duration
+ """
+ return Duration(seconds=self.total_seconds())
+
+ def __iter__(self):
+ return self.range("days")
+
+ def __contains__(self, item):
+ return self.start <= item <= self.end
+
+ def __add__(self, other):
+ return self.as_interval().__add__(other)
+
+ __radd__ = __add__
+
+ def __sub__(self, other):
+ return self.as_interval().__sub__(other)
+
+ def __neg__(self):
+ return self.__class__(self.end, self.start, self._absolute)
+
+ def __mul__(self, other):
+ return self.as_interval().__mul__(other)
+
+ __rmul__ = __mul__
+
+ def __floordiv__(self, other):
+ return self.as_interval().__floordiv__(other)
+
+ def __truediv__(self, other):
+ return self.as_interval().__truediv__(other)
+
+ __div__ = __floordiv__
+
+ def __mod__(self, other):
+ return self.as_interval().__mod__(other)
+
+ def __divmod__(self, other):
+ return self.as_interval().__divmod__(other)
+
+ def __abs__(self):
+ return self.__class__(self.start, self.end, True)
+
+ def __repr__(self):
+ return "<Period [{} -> {}]>".format(self._start, self._end)
+
+ def __str__(self):
+ return self.__repr__()
+
+ def _cmp(self, other):
+ # Only needed for PyPy
+ assert isinstance(other, timedelta)
+
+ if isinstance(other, Period):
+ other = other.as_timedelta()
+
+ td = self.as_timedelta()
+
+ return 0 if td == other else 1 if td > other else -1
+
+ def _getstate(self, protocol=3):
+ start, end = self.start, self.end
+
+ if self._invert and self._absolute:
+ end, start = start, end
+
+ return (start, end, self._absolute)
+
+ def __reduce__(self):
+ return self.__reduce_ex__(2)
+
+ def __reduce_ex__(self, protocol):
+ return self.__class__, self._getstate(protocol)
+
+ def __hash__(self):
+ return hash((self.start, self.end, self._absolute))
+
+ def __eq__(self, other):
+ if isinstance(other, Period):
+ return (self.start, self.end, self._absolute) == (
+ other.start,
+ other.end,
+ other._absolute,
+ )
+ else:
+ return self.as_interval() == other
diff --git a/pendulum/py.typed b/pendulum/py.typed
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/py.typed
diff --git a/pendulum/time.py b/pendulum/time.py
new file mode 100644
index 0000000..e72972d
--- /dev/null
+++ b/pendulum/time.py
@@ -0,0 +1,284 @@
+from __future__ import absolute_import
+
+from datetime import time
+from datetime import timedelta
+
+import pendulum
+
+from .constants import SECS_PER_HOUR
+from .constants import SECS_PER_MIN
+from .constants import USECS_PER_SEC
+from .duration import AbsoluteDuration
+from .duration import Duration
+from .mixins.default import FormattableMixin
+
+
+class Time(FormattableMixin, time):
+ """
+ Represents a time instance as hour, minute, second, microsecond.
+ """
+
+ # String formatting
+ def __repr__(self):
+ us = ""
+ if self.microsecond:
+ us = ", {}".format(self.microsecond)
+
+ tzinfo = ""
+ if self.tzinfo:
+ tzinfo = ", tzinfo={}".format(repr(self.tzinfo))
+
+ return "{}({}, {}, {}{}{})".format(
+ self.__class__.__name__, self.hour, self.minute, self.second, us, tzinfo
+ )
+
+ # Comparisons
+
+ def closest(self, dt1, dt2):
+ """
+ Get the closest time from the instance.
+
+ :type dt1: Time or time
+ :type dt2: Time or time
+
+ :rtype: Time
+ """
+ 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, dt2):
+ """
+ Get the farthest time from the instance.
+
+ :type dt1: Time or time
+ :type dt2: Time or time
+
+ :rtype: Time
+ """
+ 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=0, minutes=0, seconds=0, microseconds=0):
+ """
+ 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 .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=0, minutes=0, seconds=0, microseconds=0):
+ """
+ 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 .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):
+ """
+ Add timedelta duration to the instance.
+
+ :param delta: The timedelta instance
+ :type delta: datetime.timedelta
+
+ :rtype: Time
+ """
+ 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):
+ """
+ Remove timedelta duration from the instance.
+
+ :param delta: The timedelta instance
+ :type delta: datetime.timedelta
+
+ :rtype: Time
+ """
+ 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):
+ if not isinstance(other, timedelta):
+ return NotImplemented
+
+ return self.add_timedelta(other)
+
+ def __sub__(self, other):
+ 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)
+
+ def __rsub__(self, other):
+ 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=None, abs=True):
+ """
+ Returns the difference between two Time objects as an Duration.
+
+ :type dt: Time or None
+
+ :param abs: Whether to return an absolute interval or not
+ :type abs: bool
+
+ :rtype: Duration
+ """
+ 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=None, absolute=False, locale=None):
+ """
+ Get the difference in a human readable format in the current locale.
+
+ :type other: Time or time
+
+ :param absolute: removes time difference modifiers ago, after, etc
+ :type absolute: bool
+
+ :param locale: The locale to use for localization
+ :type locale: str
+
+ :rtype: str
+ """
+ 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=None, minute=None, second=None, microsecond=None, tzinfo=True
+ ):
+ 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(Time, self).replace(hour, minute, second, microsecond, tzinfo=tzinfo)
+ return self.__class__(
+ t.hour, t.minute, t.second, t.microsecond, tzinfo=t.tzinfo
+ )
+
+ def __getnewargs__(self):
+ return (self,)
+
+ def _get_state(self, protocol=3):
+ tz = self.tzinfo
+
+ return (self.hour, self.minute, self.second, self.microsecond, tz)
+
+ def __reduce__(self):
+ return self.__reduce_ex__(2)
+
+ def __reduce_ex__(self, protocol):
+ 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/pendulum/tz/__init__.py b/pendulum/tz/__init__.py
new file mode 100644
index 0000000..b085f37
--- /dev/null
+++ b/pendulum/tz/__init__.py
@@ -0,0 +1,60 @@
+from typing import Tuple
+from typing import Union
+
+import pytzdata
+
+from .local_timezone import get_local_timezone
+from .local_timezone import set_local_timezone
+from .local_timezone import test_local_timezone
+from .timezone import UTC
+from .timezone import FixedTimezone as _FixedTimezone
+from .timezone import Timezone as _Timezone
+
+
+PRE_TRANSITION = "pre"
+POST_TRANSITION = "post"
+TRANSITION_ERROR = "error"
+
+timezones = pytzdata.timezones # type: Tuple[str, ...]
+
+
+_tz_cache = {}
+
+
+def timezone(name, extended=True): # type: (Union[str, int], bool) -> _Timezone
+ """
+ Return a Timezone instance given its name.
+ """
+ if isinstance(name, int):
+ return fixed_timezone(name)
+
+ if name.lower() == "utc":
+ return UTC
+
+ if name in _tz_cache:
+ return _tz_cache[name]
+
+ tz = _Timezone(name, extended=extended)
+ _tz_cache[name] = tz
+
+ return tz
+
+
+def fixed_timezone(offset): # type: (int) -> _FixedTimezone
+ """
+ Return a Timezone instance given its offset in seconds.
+ """
+ if offset in _tz_cache:
+ return _tz_cache[offset] # type: ignore
+
+ tz = _FixedTimezone(offset)
+ _tz_cache[offset] = tz
+
+ return tz
+
+
+def local_timezone(): # type: () -> _Timezone
+ """
+ Return the local timezone.
+ """
+ return get_local_timezone()
diff --git a/pendulum/tz/data/__init__.py b/pendulum/tz/data/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/tz/data/__init__.py
diff --git a/pendulum/tz/data/windows.py b/pendulum/tz/data/windows.py
new file mode 100644
index 0000000..7fb5b32
--- /dev/null
+++ b/pendulum/tz/data/windows.py
@@ -0,0 +1,137 @@
+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/pendulum/tz/exceptions.py b/pendulum/tz/exceptions.py
new file mode 100644
index 0000000..d1572f9
--- /dev/null
+++ b/pendulum/tz/exceptions.py
@@ -0,0 +1,23 @@
+class TimezoneError(ValueError):
+
+ pass
+
+
+class NonExistingTime(TimezoneError):
+
+ message = "The datetime {} does not exist."
+
+ def __init__(self, dt):
+ message = self.message.format(dt)
+
+ super(NonExistingTime, self).__init__(message)
+
+
+class AmbiguousTime(TimezoneError):
+
+ message = "The datetime {} is ambiguous."
+
+ def __init__(self, dt):
+ message = self.message.format(dt)
+
+ super(AmbiguousTime, self).__init__(message)
diff --git a/pendulum/tz/local_timezone.py b/pendulum/tz/local_timezone.py
new file mode 100644
index 0000000..756105a
--- /dev/null
+++ b/pendulum/tz/local_timezone.py
@@ -0,0 +1,257 @@
+import os
+import re
+import sys
+
+from contextlib import contextmanager
+from typing import Iterator
+from typing import Optional
+from typing import Union
+
+from .timezone import Timezone
+from .timezone import TimezoneFile
+from .zoneinfo.exceptions import InvalidTimezone
+
+
+try:
+ import _winreg as winreg
+except ImportError:
+ try:
+ import winreg
+ except ImportError:
+ winreg = None
+
+
+_mock_local_timezone = None
+_local_timezone = None
+
+
+def get_local_timezone(): # type: () -> Timezone
+ 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=None): # type: (Optional[Union[str, Timezone]]) -> None
+ global _mock_local_timezone
+
+ _mock_local_timezone = mock
+
+
+@contextmanager
+def test_local_timezone(mock): # type: (Timezone) -> Iterator[None]
+ set_local_timezone(mock)
+
+ yield
+
+ set_local_timezone()
+
+
+def _get_system_timezone(): # type: () -> Timezone
+ if sys.platform == "win32":
+ return _get_windows_timezone()
+ elif "darwin" in sys.platform:
+ return _get_darwin_timezone()
+
+ return _get_unix_timezone()
+
+
+def _get_windows_timezone(): # type: () -> Timezone
+ from .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()
+ try:
+ if info["Std"] == tzwin:
+ tzkeyname = subkey
+ break
+ except KeyError:
+ # This timezone didn't have proper configuration.
+ # Ignore it.
+ pass
+
+ 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)
+
+
+def _get_darwin_timezone(): # type: () -> 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="/"): # type: (str) -> Timezone
+ tzenv = os.environ.get("TZ")
+ if tzenv:
+ try:
+ return _tz_from_env(tzenv)
+ except ValueError:
+ pass
+
+ # Now look for distribution specific configuration files
+ # that contain the timezone name.
+ tzpath = os.path.join(_root, "etc/timezone")
+ if os.path.exists(tzpath):
+ with open(tzpath, "rb") as 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 data[:5] != "TZif2":
+ etctz = 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.exists(tzpath):
+ continue
+
+ with open(tzpath, "rt") 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[: end_re.search(line).start()]
+
+ parts = list(reversed(etctz.replace(" ", "_").split(os.path.sep)))
+ tzpath = []
+ while parts:
+ tzpath.insert(0, parts.pop(0))
+
+ try:
+ return Timezone(os.path.join(*tzpath))
+ except InvalidTimezone:
+ pass
+
+ # 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.exists(tzpath) and os.path.islink(tzpath):
+ parts = list(
+ reversed(os.path.realpath(tzpath).replace(" ", "_").split(os.path.sep))
+ )
+ tzpath = []
+ while parts:
+ tzpath.insert(0, parts.pop(0))
+ try:
+ return Timezone(os.path.join(*tzpath))
+ except InvalidTimezone:
+ pass
+
+ # 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.exists(tzpath):
+ continue
+
+ return TimezoneFile(tzpath)
+
+ raise RuntimeError("Unable to find any timezone configuration")
+
+
+def _tz_from_env(tzenv): # type: (str) -> Timezone
+ if tzenv[0] == ":":
+ tzenv = tzenv[1:]
+
+ # TZ specifies a file
+ if os.path.exists(tzenv):
+ return TimezoneFile(tzenv)
+
+ # TZ specifies a zoneinfo zone.
+ try:
+ return Timezone(tzenv)
+ except ValueError:
+ raise
diff --git a/pendulum/tz/timezone.py b/pendulum/tz/timezone.py
new file mode 100644
index 0000000..bc94d56
--- /dev/null
+++ b/pendulum/tz/timezone.py
@@ -0,0 +1,377 @@
+from datetime import datetime
+from datetime import timedelta
+from datetime import tzinfo
+from typing import Optional
+from typing import TypeVar
+from typing import overload
+
+import pendulum
+
+from pendulum.helpers import local_time
+from pendulum.helpers import timestamp
+from pendulum.utils._compat import _HAS_FOLD
+
+from .exceptions import AmbiguousTime
+from .exceptions import NonExistingTime
+from .zoneinfo import read
+from .zoneinfo import read_file
+from .zoneinfo.transition import Transition
+
+
+POST_TRANSITION = "post"
+PRE_TRANSITION = "pre"
+TRANSITION_ERROR = "error"
+
+_datetime = datetime
+_D = TypeVar("_D", bound=datetime)
+
+
+class Timezone(tzinfo):
+ """
+ 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 __init__(self, name, extended=True): # type: (str, bool) -> None
+ tz = read(name, extend=extended)
+
+ self._name = name
+ self._transitions = tz.transitions
+ self._hint = {True: None, False: None}
+
+ @property
+ def name(self): # type: () -> str
+ return self._name
+
+ def convert(self, dt, dst_rule=None): # type: (_D, Optional[str]) -> _D
+ """
+ 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:
+ return self._normalize(dt, dst_rule=dst_rule)
+
+ return self._convert(dt)
+
+ def datetime(
+ self, year, month, day, hour=0, minute=0, second=0, microsecond=0
+ ): # type: (int, int, int, int, int, int, int) -> _datetime
+ """
+ Return a normalized datetime for the current timezone.
+ """
+ if _HAS_FOLD:
+ return self.convert(
+ datetime(year, month, day, hour, minute, second, microsecond, fold=1)
+ )
+
+ return self.convert(
+ datetime(year, month, day, hour, minute, second, microsecond),
+ dst_rule=POST_TRANSITION,
+ )
+
+ def _normalize(self, dt, dst_rule=None): # type: (_D, Optional[str]) -> _D
+ sec = timestamp(dt)
+ fold = 0
+ transition = self._lookup_transition(sec)
+
+ if not _HAS_FOLD and dst_rule is None:
+ dst_rule = POST_TRANSITION
+
+ if dst_rule is None:
+ dst_rule = PRE_TRANSITION
+ if dt.fold == 1:
+ dst_rule = POST_TRANSITION
+
+ if sec < transition.local:
+ if transition.is_ambiguous(sec):
+ # Ambiguous time
+ if dst_rule == TRANSITION_ERROR:
+ raise AmbiguousTime(dt)
+
+ # We set the fold attribute for later
+ if dst_rule == POST_TRANSITION:
+ fold = 1
+ elif transition.previous is not None:
+ transition = transition.previous
+
+ if transition:
+ if transition.is_ambiguous(sec):
+ # Ambiguous time
+ if dst_rule == TRANSITION_ERROR:
+ raise AmbiguousTime(dt)
+
+ # We set the fold attribute for later
+ if dst_rule == POST_TRANSITION:
+ fold = 1
+ elif transition.is_missing(sec):
+ # Skipped time
+ if dst_rule == TRANSITION_ERROR:
+ raise NonExistingTime(dt)
+
+ # We adjust accordingly
+ if dst_rule == POST_TRANSITION:
+ sec += transition.fix
+ fold = 1
+ else:
+ sec -= transition.fix
+
+ kwargs = {"tzinfo": self}
+ if _HAS_FOLD or isinstance(dt, pendulum.DateTime):
+ kwargs["fold"] = fold
+
+ return dt.__class__(*local_time(sec, 0, dt.microsecond), **kwargs)
+
+ def _convert(self, dt): # type: (_D) -> _D
+ if dt.tzinfo is self:
+ return self._normalize(dt, dst_rule=POST_TRANSITION)
+
+ if not isinstance(dt.tzinfo, Timezone):
+ return dt.astimezone(self)
+
+ stamp = timestamp(dt)
+
+ if isinstance(dt.tzinfo, FixedTimezone):
+ offset = dt.tzinfo.offset
+ else:
+ transition = dt.tzinfo._lookup_transition(stamp)
+ offset = transition.ttype.offset
+
+ if stamp < transition.local and transition.previous is not None:
+ if (
+ transition.previous.is_ambiguous(stamp)
+ and getattr(dt, "fold", 1) == 0
+ ):
+ pass
+ else:
+ offset = transition.previous.ttype.offset
+
+ stamp -= offset
+
+ transition = self._lookup_transition(stamp, is_utc=True)
+ if stamp < transition.at and transition.previous is not None:
+ transition = transition.previous
+
+ offset = transition.ttype.offset
+ stamp += offset
+ fold = int(not transition.ttype.is_dst())
+
+ kwargs = {"tzinfo": self}
+
+ if _HAS_FOLD or isinstance(dt, pendulum.DateTime):
+ kwargs["fold"] = fold
+
+ return dt.__class__(*local_time(stamp, 0, dt.microsecond), **kwargs)
+
+ def _lookup_transition(
+ self, stamp, is_utc=False
+ ): # type: (int, bool) -> Transition
+ lo, hi = 0, len(self._transitions)
+ hint = self._hint[is_utc]
+ if hint:
+ if stamp == hint[0]:
+ return self._transitions[hint[1]]
+ elif stamp < hint[0]:
+ hi = hint[1]
+ else:
+ lo = hint[1]
+
+ if not is_utc:
+ while lo < hi:
+ mid = (lo + hi) // 2
+ if stamp < self._transitions[mid].to:
+ hi = mid
+ else:
+ lo = mid + 1
+ else:
+ while lo < hi:
+ mid = (lo + hi) // 2
+ if stamp < self._transitions[mid].at:
+ hi = mid
+ else:
+ lo = mid + 1
+
+ if lo >= len(self._transitions):
+ # Beyond last transition
+ lo = len(self._transitions) - 1
+
+ self._hint[is_utc] = (stamp, lo)
+
+ return self._transitions[lo]
+
+ @overload
+ def utcoffset(self, dt): # type: (None) -> None
+ pass
+
+ @overload
+ def utcoffset(self, dt): # type: (_datetime) -> timedelta
+ pass
+
+ def utcoffset(self, dt):
+ if dt is None:
+ return
+
+ transition = self._get_transition(dt)
+
+ return transition.utcoffset()
+
+ def dst(
+ self, dt # type: Optional[_datetime]
+ ): # type: (...) -> Optional[timedelta]
+ if dt is None:
+ return
+
+ transition = self._get_transition(dt)
+
+ if not transition.ttype.is_dst():
+ return timedelta()
+
+ return timedelta(seconds=transition.fix)
+
+ def tzname(self, dt): # type: (Optional[_datetime]) -> Optional[str]
+ if dt is None:
+ return
+
+ transition = self._get_transition(dt)
+
+ return transition.ttype.abbreviation
+
+ def _get_transition(self, dt): # type: (_datetime) -> Transition
+ if dt.tzinfo is not None and dt.tzinfo is not self:
+ dt = dt - dt.utcoffset()
+
+ stamp = timestamp(dt)
+
+ transition = self._lookup_transition(stamp, is_utc=True)
+ else:
+ stamp = timestamp(dt)
+
+ transition = self._lookup_transition(stamp)
+
+ if stamp < transition.local and transition.previous is not None:
+ fold = getattr(dt, "fold", 1)
+ if transition.is_ambiguous(stamp):
+ if fold == 0:
+ transition = transition.previous
+ elif transition.previous.is_ambiguous(stamp) and fold == 0:
+ pass
+ else:
+ transition = transition.previous
+
+ return transition
+
+ def fromutc(self, dt): # type: (_D) -> _D
+ stamp = timestamp(dt)
+
+ transition = self._lookup_transition(stamp, is_utc=True)
+ if stamp < transition.at and transition.previous is not None:
+ transition = transition.previous
+
+ stamp += transition.ttype.offset
+
+ return dt.__class__(*local_time(stamp, 0, dt.microsecond), tzinfo=self)
+
+ def __repr__(self): # type: () -> str
+ return "Timezone('{}')".format(self._name)
+
+ def __getinitargs__(self): # type: () -> tuple
+ return (self._name,)
+
+
+class FixedTimezone(Timezone):
+ def __init__(self, offset, name=None):
+ sign = "-" if offset < 0 else "+"
+
+ minutes = offset / 60
+ hour, minute = divmod(abs(int(minutes)), 60)
+
+ if not name:
+ name = "{0}{1:02d}:{2:02d}".format(sign, hour, minute)
+
+ self._name = name
+ self._offset = offset
+ self._utcoffset = timedelta(seconds=offset)
+
+ @property
+ def offset(self): # type: () -> int
+ return self._offset
+
+ def _normalize(self, dt, dst_rule=None): # type: (_D, Optional[str]) -> _D
+ if _HAS_FOLD:
+ dt = dt.__class__(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ tzinfo=self,
+ fold=0,
+ )
+ else:
+ dt = dt.__class__(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ tzinfo=self,
+ )
+
+ return dt
+
+ def _convert(self, dt): # type: (_D) -> _D
+ if dt.tzinfo is not self:
+ return dt.astimezone(self)
+
+ return dt
+
+ def utcoffset(self, dt): # type: (Optional[_datetime]) -> timedelta
+ return self._utcoffset
+
+ def dst(self, dt): # type: (Optional[_datetime]) -> timedelta
+ return timedelta()
+
+ def fromutc(self, dt): # type: (_D) -> _D
+ # Use the stdlib datetime's add method to avoid infinite recursion
+ return (datetime.__add__(dt, self._utcoffset)).replace(tzinfo=self)
+
+ def tzname(self, dt): # type: (Optional[_datetime]) -> Optional[str]
+ return self._name
+
+ def __getinitargs__(self): # type: () -> tuple
+ return self._offset, self._name
+
+
+class TimezoneFile(Timezone):
+ def __init__(self, path):
+ tz = read_file(path)
+
+ self._name = ""
+ self._transitions = tz.transitions
+ self._hint = {True: None, False: None}
+
+
+UTC = FixedTimezone(0, "UTC")
diff --git a/pendulum/tz/zoneinfo/__init__.py b/pendulum/tz/zoneinfo/__init__.py
new file mode 100644
index 0000000..890351a
--- /dev/null
+++ b/pendulum/tz/zoneinfo/__init__.py
@@ -0,0 +1,16 @@
+from .reader import Reader
+from .timezone import Timezone
+
+
+def read(name, extend=True): # type: (str, bool) -> Timezone
+ """
+ Read the zoneinfo structure for a given timezone name.
+ """
+ return Reader(extend=extend).read_for(name)
+
+
+def read_file(path, extend=True): # type: (str, bool) -> Timezone
+ """
+ Read the zoneinfo structure for a given path.
+ """
+ return Reader(extend=extend).read(path)
diff --git a/pendulum/tz/zoneinfo/exceptions.py b/pendulum/tz/zoneinfo/exceptions.py
new file mode 100644
index 0000000..6e29ae2
--- /dev/null
+++ b/pendulum/tz/zoneinfo/exceptions.py
@@ -0,0 +1,18 @@
+class ZoneinfoError(Exception):
+
+ pass
+
+
+class InvalidZoneinfoFile(ZoneinfoError):
+
+ pass
+
+
+class InvalidTimezone(ZoneinfoError):
+ def __init__(self, name):
+ super(InvalidTimezone, self).__init__('Invalid timezone "{}"'.format(name))
+
+
+class InvalidPosixSpec(ZoneinfoError):
+ def __init__(self, spec):
+ super(InvalidPosixSpec, self).__init__("Invalid POSIX spec: {}".format(spec))
diff --git a/pendulum/tz/zoneinfo/posix_timezone.py b/pendulum/tz/zoneinfo/posix_timezone.py
new file mode 100644
index 0000000..a6a7c72
--- /dev/null
+++ b/pendulum/tz/zoneinfo/posix_timezone.py
@@ -0,0 +1,270 @@
+"""
+Parsing of a POSIX zone spec as described in the TZ part of section 8.3 in
+http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html.
+"""
+import re
+
+from typing import Optional
+
+from pendulum.constants import MONTHS_OFFSETS
+from pendulum.constants import SECS_PER_DAY
+
+from .exceptions import InvalidPosixSpec
+
+
+_spec = re.compile(
+ "^"
+ r"(?P<std_abbr><.*?>|[^-+,\d]{3,})"
+ r"(?P<std_offset>([+-])?(\d{1,2})(:\d{2}(:\d{2})?)?)"
+ r"(?P<dst_info>"
+ r" (?P<dst_abbr><.*?>|[^-+,\d]{3,})"
+ r" (?P<dst_offset>([+-])?(\d{1,2})(:\d{2}(:\d{2})?)?)?"
+ r")?"
+ r"(?:,(?P<rules>"
+ r" (?P<dst_start>"
+ r" (?:J\d+|\d+|M\d{1,2}.\d.[0-6])"
+ r" (?:/(?P<dst_start_offset>([+-])?(\d+)(:\d{2}(:\d{2})?)?))?"
+ " )"
+ " ,"
+ r" (?P<dst_end>"
+ r" (?:J\d+|\d+|M\d{1,2}.\d.[0-6])"
+ r" (?:/(?P<dst_end_offset>([+-])?(\d+)(:\d{2}(:\d{2})?)?))?"
+ " )"
+ "))?"
+ "$",
+ re.VERBOSE,
+)
+
+
+def posix_spec(spec): # type: (str) -> PosixTimezone
+ try:
+ return _posix_spec(spec)
+ except ValueError:
+ raise InvalidPosixSpec(spec)
+
+
+def _posix_spec(spec): # type: (str) -> PosixTimezone
+ m = _spec.match(spec)
+ if not m:
+ raise ValueError("Invalid posix spec")
+
+ std_abbr = _parse_abbr(m.group("std_abbr"))
+ std_offset = _parse_offset(m.group("std_offset"))
+
+ dst_abbr = None
+ dst_offset = None
+ if m.group("dst_info"):
+ dst_abbr = _parse_abbr(m.group("dst_abbr"))
+ if m.group("dst_offset"):
+ dst_offset = _parse_offset(m.group("dst_offset"))
+ else:
+ dst_offset = std_offset + 3600
+
+ dst_start = None
+ dst_end = None
+ if m.group("rules"):
+ dst_start = _parse_rule(m.group("dst_start"))
+ dst_end = _parse_rule(m.group("dst_end"))
+
+ return PosixTimezone(std_abbr, std_offset, dst_abbr, dst_offset, dst_start, dst_end)
+
+
+def _parse_abbr(text): # type: (str) -> str
+ return text.lstrip("<").rstrip(">")
+
+
+def _parse_offset(text, sign=-1): # type: (str, int) -> int
+ if text.startswith(("+", "-")):
+ if text.startswith("-"):
+ sign *= -1
+
+ text = text[1:]
+
+ minutes = 0
+ seconds = 0
+
+ parts = text.split(":")
+ hours = int(parts[0])
+
+ if len(parts) > 1:
+ minutes = int(parts[1])
+
+ if len(parts) > 2:
+ seconds = int(parts[2])
+
+ return sign * ((((hours * 60) + minutes) * 60) + seconds)
+
+
+def _parse_rule(rule): # type: (str) -> PosixTransition
+ klass = NPosixTransition
+ args = ()
+
+ if rule.startswith("M"):
+ rule = rule[1:]
+ parts = rule.split(".")
+ month = int(parts[0])
+ week = int(parts[1])
+ day = int(parts[2].split("/")[0])
+
+ args += (month, week, day)
+ klass = MPosixTransition
+ elif rule.startswith("J"):
+ rule = rule[1:]
+ args += (int(rule.split("/")[0]),)
+ klass = JPosixTransition
+ else:
+ args += (int(rule.split("/")[0]),)
+
+ # Checking offset
+ parts = rule.split("/")
+ if len(parts) > 1:
+ offset = _parse_offset(parts[-1], sign=1)
+ else:
+ offset = 7200
+
+ args += (offset,)
+
+ return klass(*args)
+
+
+class PosixTransition(object):
+ def __init__(self, offset): # type: (int) -> None
+ self._offset = offset
+
+ @property
+ def offset(self): # type: () -> int
+ return self._offset
+
+ def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int
+ raise NotImplementedError()
+
+
+class JPosixTransition(PosixTransition):
+ def __init__(self, day, offset): # type: (int, int) -> None
+ self._day = day
+
+ super(JPosixTransition, self).__init__(offset)
+
+ @property
+ def day(self): # type: () -> int
+ """
+ day of non-leap year [1:365]
+ """
+ return self._day
+
+ def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int
+ days = self._day
+ if not is_leap or days < MONTHS_OFFSETS[1][3]:
+ days -= 1
+
+ return (days * SECS_PER_DAY) + self._offset
+
+
+class NPosixTransition(PosixTransition):
+ def __init__(self, day, offset): # type: (int, int) -> None
+ self._day = day
+
+ super(NPosixTransition, self).__init__(offset)
+
+ @property
+ def day(self): # type: () -> int
+ """
+ day of year [0:365]
+ """
+ return self._day
+
+ def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int
+ days = self._day
+
+ return (days * SECS_PER_DAY) + self._offset
+
+
+class MPosixTransition(PosixTransition):
+ def __init__(self, month, week, weekday, offset):
+ # type: (int, int, int, int) -> None
+ self._month = month
+ self._week = week
+ self._weekday = weekday
+
+ super(MPosixTransition, self).__init__(offset)
+
+ @property
+ def month(self): # type: () -> int
+ """
+ month of year [1:12]
+ """
+ return self._month
+
+ @property
+ def week(self): # type: () -> int
+ """
+ week of month [1:5] (5==last)
+ """
+ return self._week
+
+ @property
+ def weekday(self): # type: () -> int
+ """
+ 0==Sun, ..., 6=Sat
+ """
+ return self._weekday
+
+ def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int
+ last_week = self._week == 5
+ days = MONTHS_OFFSETS[is_leap][self._month + int(last_week)]
+ weekday = (jan1_weekday + days) % 7
+ if last_week:
+ days -= (weekday + 7 - 1 - self._weekday) % 7 + 1
+ else:
+ days += (self._weekday + 7 - weekday) % 7
+ days += (self._week - 1) * 7
+
+ return (days * SECS_PER_DAY) + self._offset
+
+
+class PosixTimezone:
+ """
+ The entirety of a POSIX-string specified time-zone rule.
+
+ The standard abbreviation and offset are always given.
+ """
+
+ def __init__(
+ self,
+ std_abbr, # type: str
+ std_offset, # type: int
+ dst_abbr, # type: Optional[str]
+ dst_offset, # type: Optional[int]
+ dst_start=None, # type: Optional[PosixTransition]
+ dst_end=None, # type: Optional[PosixTransition]
+ ):
+ self._std_abbr = std_abbr
+ self._std_offset = std_offset
+ self._dst_abbr = dst_abbr
+ self._dst_offset = dst_offset
+ self._dst_start = dst_start
+ self._dst_end = dst_end
+
+ @property
+ def std_abbr(self): # type: () -> str
+ return self._std_abbr
+
+ @property
+ def std_offset(self): # type: () -> int
+ return self._std_offset
+
+ @property
+ def dst_abbr(self): # type: () -> Optional[str]
+ return self._dst_abbr
+
+ @property
+ def dst_offset(self): # type: () -> Optional[int]
+ return self._dst_offset
+
+ @property
+ def dst_start(self): # type: () -> Optional[PosixTransition]
+ return self._dst_start
+
+ @property
+ def dst_end(self): # type: () -> Optional[PosixTransition]
+ return self._dst_end
diff --git a/pendulum/tz/zoneinfo/reader.py b/pendulum/tz/zoneinfo/reader.py
new file mode 100644
index 0000000..31cb933
--- /dev/null
+++ b/pendulum/tz/zoneinfo/reader.py
@@ -0,0 +1,224 @@
+import os
+
+from collections import namedtuple
+from struct import unpack
+from typing import IO
+from typing import Any
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import Tuple
+
+import pytzdata
+
+from pytzdata.exceptions import TimezoneNotFound
+
+from pendulum.utils._compat import PY2
+
+from .exceptions import InvalidTimezone
+from .exceptions import InvalidZoneinfoFile
+from .posix_timezone import PosixTimezone
+from .posix_timezone import posix_spec
+from .timezone import Timezone
+from .transition import Transition
+from .transition_type import TransitionType
+
+
+_offset = namedtuple("offset", "utc_total_offset is_dst abbr_idx")
+
+header = namedtuple(
+ "header",
+ "version " "utclocals " "stdwalls " "leaps " "transitions " "types " "abbr_size",
+)
+
+
+class Reader:
+ """
+ Reads compiled zoneinfo TZif (\0, 2 or 3) files.
+ """
+
+ def __init__(self, extend=True): # type: (bool) -> None
+ self._extend = extend
+
+ def read_for(self, timezone): # type: (str) -> Timezone
+ """
+ Read the zoneinfo structure for a given timezone name.
+
+ :param timezone: The timezone.
+ """
+ try:
+ file_path = pytzdata.tz_path(timezone)
+ except TimezoneNotFound:
+ raise InvalidTimezone(timezone)
+
+ return self.read(file_path)
+
+ def read(self, file_path): # type: (str) -> Timezone
+ """
+ Read a zoneinfo structure from the given path.
+
+ :param file_path: The path of a zoneinfo file.
+ """
+ if not os.path.exists(file_path):
+ raise InvalidZoneinfoFile("The tzinfo file does not exist")
+
+ with open(file_path, "rb") as fd:
+ return self._parse(fd)
+
+ def _check_read(self, fd, nbytes): # type: (...) -> bytes
+ """
+ Reads the given number of bytes from the given file
+ and checks that the correct number of bytes could be read.
+ """
+ result = fd.read(nbytes)
+
+ if (not result and nbytes > 0) or len(result) != nbytes:
+ raise InvalidZoneinfoFile(
+ "Expected {} bytes reading {}, "
+ "but got {}".format(nbytes, fd.name, len(result) if result else 0)
+ )
+
+ if PY2:
+ return bytearray(result)
+
+ return result
+
+ def _parse(self, fd): # type: (...) -> Timezone
+ """
+ Parse a zoneinfo file.
+ """
+ hdr = self._parse_header(fd)
+
+ if hdr.version in (2, 3):
+ # We're skipping the entire v1 file since
+ # at least the same data will be found in TZFile 2.
+ fd.seek(
+ hdr.transitions * 5
+ + hdr.types * 6
+ + hdr.abbr_size
+ + hdr.leaps * 4
+ + hdr.stdwalls
+ + hdr.utclocals,
+ 1,
+ )
+
+ # Parse the second header
+ hdr = self._parse_header(fd)
+
+ if hdr.version != 2 and hdr.version != 3:
+ raise InvalidZoneinfoFile(
+ "Header versions mismatch for file {}".format(fd.name)
+ )
+
+ # Parse the v2 data
+ trans = self._parse_trans_64(fd, hdr.transitions)
+ type_idx = self._parse_type_idx(fd, hdr.transitions)
+ types = self._parse_types(fd, hdr.types)
+ abbrs = self._parse_abbrs(fd, hdr.abbr_size, types)
+
+ fd.seek(hdr.leaps * 8 + hdr.stdwalls + hdr.utclocals, 1)
+
+ trule = self._parse_posix_tz(fd)
+ else:
+ # TZFile v1
+ trans = self._parse_trans_32(fd, hdr.transitions)
+ type_idx = self._parse_type_idx(fd, hdr.transitions)
+ types = self._parse_types(fd, hdr.types)
+ abbrs = self._parse_abbrs(fd, hdr.abbr_size, types)
+ trule = None
+
+ types = [
+ TransitionType(off, is_dst, abbrs[abbr]) for off, is_dst, abbr in types
+ ]
+
+ transitions = []
+ previous = None
+ for trans, idx in zip(trans, type_idx):
+ transition = Transition(trans, types[idx], previous)
+ transitions.append(transition)
+
+ previous = transition
+
+ if not transitions:
+ transitions.append(Transition(0, types[0], None))
+
+ return Timezone(transitions, posix_rule=trule, extended=self._extend)
+
+ def _parse_header(self, fd): # type: (...) -> header
+ buff = self._check_read(fd, 44)
+
+ if buff[:4] != b"TZif":
+ raise InvalidZoneinfoFile(
+ 'The file "{}" has an invalid header.'.format(fd.name)
+ )
+
+ version = {0x00: 1, 0x32: 2, 0x33: 3}.get(buff[4])
+
+ if version is None:
+ raise InvalidZoneinfoFile(
+ 'The file "{}" has an invalid version.'.format(fd.name)
+ )
+
+ hdr = header(version, *unpack(">6l", buff[20:44]))
+
+ return hdr
+
+ def _parse_trans_64(self, fd, n): # type: (IO[Any], int) -> List[int]
+ trans = []
+ for _ in range(n):
+ buff = self._check_read(fd, 8)
+ trans.append(unpack(">q", buff)[0])
+
+ return trans
+
+ def _parse_trans_32(self, fd, n): # type: (IO[Any], int) -> List[int]
+ trans = []
+ for _ in range(n):
+ buff = self._check_read(fd, 4)
+ trans.append(unpack(">i", buff)[0])
+
+ return trans
+
+ def _parse_type_idx(self, fd, n): # type: (IO[Any], int) -> List[int]
+ buff = self._check_read(fd, n)
+
+ return list(unpack("{}B".format(n), buff))
+
+ def _parse_types(
+ self, fd, n
+ ): # type: (IO[Any], int) -> List[Tuple[Any, bool, int]]
+ types = []
+
+ for _ in range(n):
+ buff = self._check_read(fd, 6)
+ offset = unpack(">l", buff[:4])[0]
+ is_dst = buff[4] == 1
+ types.append((offset, is_dst, buff[5]))
+
+ return types
+
+ def _parse_abbrs(
+ self, fd, n, types
+ ): # type: (IO[Any], int, List[Tuple[Any, bool, int]]) -> Dict[int, str]
+ abbrs = {}
+ buff = self._check_read(fd, n)
+
+ for offset, is_dst, idx in types:
+ if idx not in abbrs:
+ abbr = buff[idx : buff.find(b"\0", idx)].decode("utf-8")
+ abbrs[idx] = abbr
+
+ return abbrs
+
+ def _parse_posix_tz(self, fd): # type: (...) -> Optional[PosixTimezone]
+ s = fd.read().decode("utf-8")
+
+ if not s.startswith("\n") or not s.endswith("\n"):
+ raise InvalidZoneinfoFile('Invalid posix rule in file "{}"'.format(fd.name))
+
+ s = s.strip()
+
+ if not s:
+ return
+
+ return posix_spec(s)
diff --git a/pendulum/tz/zoneinfo/timezone.py b/pendulum/tz/zoneinfo/timezone.py
new file mode 100644
index 0000000..2147774
--- /dev/null
+++ b/pendulum/tz/zoneinfo/timezone.py
@@ -0,0 +1,128 @@
+from datetime import datetime
+from typing import List
+from typing import Optional
+
+from pendulum.constants import DAYS_PER_YEAR
+from pendulum.constants import SECS_PER_YEAR
+from pendulum.helpers import is_leap
+from pendulum.helpers import local_time
+from pendulum.helpers import timestamp
+from pendulum.helpers import week_day
+
+from .posix_timezone import PosixTimezone
+from .transition import Transition
+from .transition_type import TransitionType
+
+
+class Timezone:
+ def __init__(
+ self,
+ transitions, # type: List[Transition]
+ posix_rule=None, # type: Optional[PosixTimezone]
+ extended=True, # type: bool
+ ):
+ self._posix_rule = posix_rule
+ self._transitions = transitions
+
+ if extended:
+ self._extends()
+
+ @property
+ def transitions(self): # type: () -> List[Transition]
+ return self._transitions
+
+ @property
+ def posix_rule(self):
+ return self._posix_rule
+
+ def _extends(self):
+ if not self._posix_rule:
+ return
+
+ posix = self._posix_rule
+
+ if not posix.dst_abbr:
+ # std only
+ # The future specification should match the last/default transition
+ ttype = self._transitions[-1].ttype
+ if not self._check_ttype(ttype, posix.std_offset, False, posix.std_abbr):
+ raise ValueError("Posix spec does not match last transition")
+
+ return
+
+ if len(self._transitions) < 2:
+ raise ValueError("Too few transitions for POSIX spec")
+
+ # Extend the transitions for an additional 400 years
+ # using the future specification
+
+ # The future specification should match the last two transitions,
+ # and those transitions should have different is_dst flags.
+ tr0 = self._transitions[-1]
+ tr1 = self._transitions[-2]
+ tt0 = tr0.ttype
+ tt1 = tr1.ttype
+ if tt0.is_dst():
+ dst = tt0
+ std = tt1
+ else:
+ dst = tt1
+ std = tt0
+
+ self._check_ttype(dst, posix.dst_offset, True, posix.dst_abbr)
+ self._check_ttype(std, posix.std_offset, False, posix.std_abbr)
+
+ # Add the transitions to tr1 and back to tr0 for each extra year.
+ last_year = local_time(tr0.local, 0, 0)[0]
+ leap_year = is_leap(last_year)
+ jan1 = datetime(last_year, 1, 1)
+ jan1_time = timestamp(jan1)
+ jan1_weekday = week_day(jan1.year, jan1.month, jan1.day) % 7
+
+ if local_time(tr1.local, 0, 0)[0] != last_year:
+ # Add a single extra transition to align to a calendar year.
+ if tt0.is_dst():
+ pt1 = posix.dst_end
+ else:
+ pt1 = posix.dst_start
+
+ tr1_offset = pt1.trans_offset(leap_year, jan1_weekday)
+ tr = Transition(jan1_time + tr1_offset - tt0.offset, tr1.ttype, tr0)
+ tr0 = tr
+ tr1 = tr0
+ tt0 = tr0.ttype
+ tt1 = tr1.ttype
+
+ if tt0.is_dst():
+ pt1 = posix.dst_end
+ pt0 = posix.dst_start
+ else:
+ pt1 = posix.dst_start
+ pt0 = posix.dst_end
+
+ tr = tr0
+ for year in range(last_year + 1, last_year + 401):
+ jan1_time += SECS_PER_YEAR[leap_year]
+ jan1_weekday = (jan1_weekday + DAYS_PER_YEAR[leap_year]) % 7
+ leap_year = not leap_year and is_leap(year)
+
+ tr1_offset = pt1.trans_offset(leap_year, jan1_weekday)
+ tr = Transition(jan1_time + tr1_offset - tt0.offset, tt1, tr)
+ self._transitions.append(tr)
+
+ tr0_offset = pt0.trans_offset(leap_year, jan1_weekday)
+ tr = Transition(jan1_time + tr0_offset - tt1.offset, tt0, tr)
+ self._transitions.append(tr)
+
+ def _check_ttype(
+ self,
+ ttype, # type: TransitionType
+ offset, # type: int
+ is_dst, # type: bool
+ abbr, # type: str
+ ): # type: (...) -> bool
+ return (
+ ttype.offset == offset
+ and ttype.is_dst() == is_dst
+ and ttype.abbreviation == abbr
+ )
diff --git a/pendulum/tz/zoneinfo/transition.py b/pendulum/tz/zoneinfo/transition.py
new file mode 100644
index 0000000..7c6b2f7
--- /dev/null
+++ b/pendulum/tz/zoneinfo/transition.py
@@ -0,0 +1,77 @@
+from datetime import timedelta
+from typing import Optional
+
+from .transition_type import TransitionType
+
+
+class Transition:
+ def __init__(
+ self,
+ at, # type: int
+ ttype, # type: TransitionType
+ previous, # type: Optional[Transition]
+ ):
+ self._at = at
+
+ if previous:
+ self._local = at + previous.ttype.offset
+ else:
+ self._local = at + ttype.offset
+
+ self._ttype = ttype
+ self._previous = previous
+
+ if self.previous:
+ self._fix = self._ttype.offset - self.previous.ttype.offset
+ else:
+ self._fix = 0
+
+ self._to = self._local + self._fix
+ self._to_utc = self._at + self._fix
+ self._utcoffset = timedelta(seconds=ttype.offset)
+
+ @property
+ def at(self): # type: () -> int
+ return self._at
+
+ @property
+ def local(self): # type: () -> int
+ return self._local
+
+ @property
+ def to(self): # type: () -> int
+ return self._to
+
+ @property
+ def to_utc(self): # type: () -> int
+ return self._to
+
+ @property
+ def ttype(self): # type: () -> TransitionType
+ return self._ttype
+
+ @property
+ def previous(self): # type: () -> Optional[Transition]
+ return self._previous
+
+ @property
+ def fix(self): # type: () -> int
+ return self._fix
+
+ def is_ambiguous(self, stamp): # type: (int) -> bool
+ return self._to <= stamp < self._local
+
+ def is_missing(self, stamp): # type: (int) -> bool
+ return self._local <= stamp < self._to
+
+ def utcoffset(self): # type: () -> timedelta
+ return self._utcoffset
+
+ def __contains__(self, stamp): # type: (int) -> bool
+ if self.previous is None:
+ return stamp < self.local
+
+ return self.previous.local <= stamp < self.local
+
+ def __repr__(self): # type: () -> str
+ return "Transition({} -> {}, {})".format(self._local, self._to, self._ttype)
diff --git a/pendulum/tz/zoneinfo/transition_type.py b/pendulum/tz/zoneinfo/transition_type.py
new file mode 100644
index 0000000..dd0a634
--- /dev/null
+++ b/pendulum/tz/zoneinfo/transition_type.py
@@ -0,0 +1,35 @@
+from datetime import timedelta
+
+from pendulum.utils._compat import PY2
+from pendulum.utils._compat import encode
+
+
+class TransitionType:
+ def __init__(self, offset, is_dst, abbr):
+ self._offset = offset
+ self._is_dst = is_dst
+ self._abbr = abbr
+
+ self._utcoffset = timedelta(seconds=offset)
+
+ @property
+ def offset(self): # type: () -> int
+ return self._offset
+
+ @property
+ def abbreviation(self): # type: () -> str
+ if PY2:
+ return encode(self._abbr)
+
+ return self._abbr
+
+ def is_dst(self): # type: () -> bool
+ return self._is_dst
+
+ def utcoffset(self): # type: () -> timedelta
+ return self._utcoffset
+
+ def __repr__(self): # type: () -> str
+ return "TransitionType({}, {}, {})".format(
+ self._offset, self._is_dst, self._abbr
+ )
diff --git a/pendulum/utils/__init__.py b/pendulum/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/utils/__init__.py
diff --git a/pendulum/utils/_compat.py b/pendulum/utils/_compat.py
new file mode 100644
index 0000000..07cead1
--- /dev/null
+++ b/pendulum/utils/_compat.py
@@ -0,0 +1,54 @@
+import sys
+
+
+PY2 = sys.version_info < (3, 0)
+PY36 = sys.version_info >= (3, 6)
+PYPY = hasattr(sys, "pypy_version_info")
+
+_HAS_FOLD = PY36
+
+
+try: # Python 2
+ long = long
+ unicode = unicode
+ basestring = basestring
+except NameError: # Python 3
+ long = int
+ unicode = str
+ basestring = str
+
+
+def decode(string, encodings=None):
+ if not PY2 and not isinstance(string, bytes):
+ return string
+
+ if PY2 and isinstance(string, unicode):
+ return string
+
+ encodings = encodings or ["utf-8", "latin1", "ascii"]
+
+ for encoding in encodings:
+ try:
+ return string.decode(encoding)
+ except (UnicodeEncodeError, UnicodeDecodeError):
+ pass
+
+ return string.decode(encodings[0], errors="ignore")
+
+
+def encode(string, encodings=None):
+ if not PY2 and isinstance(string, bytes):
+ return string
+
+ if PY2 and isinstance(string, str):
+ return string
+
+ encodings = encodings or ["utf-8", "latin1", "ascii"]
+
+ for encoding in encodings:
+ try:
+ return string.encode(encoding)
+ except (UnicodeEncodeError, UnicodeDecodeError):
+ pass
+
+ return string.encode(encodings[0], errors="ignore")
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..cadf000
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,81 @@
+[tool.poetry]
+name = "pendulum"
+version = "2.1.2"
+description = "Python datetimes made easy"
+authors = ["Sébastien Eustace <sebastien@eustace.io>"]
+license = "MIT"
+readme = 'README.rst'
+homepage = "https://pendulum.eustace.io"
+repository = "https://github.com/sdispater/pendulum"
+documentation = "https://pendulum.eustace.io/docs"
+keywords = ['datetime', 'date', 'time']
+
+packages = [
+ {include = "pendulum"},
+ #{include = "tests", format = "sdist"},
+]
+include = [
+ {path = "pendulum/py.typed"},
+ # C extensions must be included in the wheel distributions
+ {path = "pendulum/_extensions/*.so", format = "wheel"},
+ {path = "pendulum/_extensions/*.pyd", format = "wheel"},
+ {path = "pendulum/parsing/*.so", format = "wheel"},
+ {path = "pendulum/parsing/*.pyd", format = "wheel"},
+]
+
+
+[tool.poetry.dependencies]
+python = "~2.7 || ^3.5"
+python-dateutil = "^2.6"
+pytzdata = ">=2020.1"
+
+# typing is needed for Python < 3.5
+typing = { version = "^3.6", python = "<3.5" }
+
+[tool.poetry.dev-dependencies]
+pytest = "^4.6"
+pytest-cov = "^2.5"
+pytz = ">=2018.3"
+babel = "^2.5"
+cleo = "^0.8.1"
+tox = "^3.0"
+black = { version = "^19.3b0", markers = "python_version >= '3.6' and python_version < '4.0' and implementation_name != 'pypy'" }
+isort = { version = "^4.3.21", markers = "python_version >= '3.6' and python_version < '4.0'" }
+pre-commit = "^1.10"
+mkdocs = { version = "^1.0", python = "^3.5" }
+pymdown-extensions = "^6.0"
+pygments = "^2.2"
+markdown-include = "^0.5.1"
+freezegun = "^0.3.15"
+
+[tool.poetry.build]
+generate-setup-file = false
+script = "build.py"
+
+[tool.isort]
+line_length = 88
+force_single_line = true
+force_grid_wrap = 0
+atomic = true
+include_trailing_comma = true
+lines_after_imports = 2
+lines_between_types = 1
+multi_line_output = 3
+use_parentheses = true
+not_skip = "__init__.py"
+skip_glob = ["*/setup.py"]
+filter_files = true
+
+known_first_party = "pendulum"
+known_third_party = [
+ "babel",
+ "cleo",
+ "dateutil",
+ "freezegun",
+ "pytzdata",
+]
+
+
+[build-system]
+requires = ["poetry-core>=1.0.0a9"]
+build-backend = "poetry.core.masonry.api"