From 1199780155f666b6806d563a29d093a251664009 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 30 Jan 2021 09:13:47 +0100 Subject: Adding upstream version 2.1.2. Signed-off-by: Daniel Baumann --- LICENSE | 20 + PKG-INFO | 251 +++++ README.rst | 224 ++++ build.py | 87 ++ pendulum/__init__.py | 315 ++++++ pendulum/__version__.py | 1 + pendulum/_extensions/__init__.py | 0 pendulum/_extensions/_helpers.c | 930 ++++++++++++++++ pendulum/_extensions/helpers.py | 358 ++++++ pendulum/constants.py | 107 ++ pendulum/date.py | 891 +++++++++++++++ pendulum/datetime.py | 1563 +++++++++++++++++++++++++++ pendulum/duration.py | 479 ++++++++ pendulum/exceptions.py | 6 + pendulum/formatting/__init__.py | 4 + pendulum/formatting/difference_formatter.py | 153 +++ pendulum/formatting/formatter.py | 685 ++++++++++++ pendulum/helpers.py | 224 ++++ pendulum/locales/__init__.py | 0 pendulum/locales/da/__init__.py | 0 pendulum/locales/da/custom.py | 22 + pendulum/locales/da/locale.py | 150 +++ pendulum/locales/de/__init__.py | 0 pendulum/locales/de/custom.py | 40 + pendulum/locales/de/locale.py | 147 +++ pendulum/locales/en/__init__.py | 0 pendulum/locales/en/custom.py | 27 + pendulum/locales/en/locale.py | 153 +++ pendulum/locales/es/__init__.py | 0 pendulum/locales/es/custom.py | 27 + pendulum/locales/es/locale.py | 144 +++ pendulum/locales/fa/__init__.py | 0 pendulum/locales/fa/custom.py | 22 + pendulum/locales/fa/locale.py | 138 +++ pendulum/locales/fo/__init__.py | 0 pendulum/locales/fo/custom.py | 24 + pendulum/locales/fo/locale.py | 135 +++ pendulum/locales/fr/__init__.py | 1 + pendulum/locales/fr/custom.py | 27 + pendulum/locales/fr/locale.py | 136 +++ pendulum/locales/id/__init__.py | 0 pendulum/locales/id/custom.py | 23 + pendulum/locales/id/locale.py | 144 +++ pendulum/locales/it/__init__.py | 0 pendulum/locales/it/custom.py | 27 + pendulum/locales/it/locale.py | 148 +++ pendulum/locales/ko/__init__.py | 0 pendulum/locales/ko/custom.py | 22 + pendulum/locales/ko/locale.py | 108 ++ pendulum/locales/locale.py | 104 ++ pendulum/locales/lt/__init__.py | 0 pendulum/locales/lt/custom.py | 122 +++ pendulum/locales/lt/locale.py | 258 +++++ pendulum/locales/nb/__init__.py | 0 pendulum/locales/nb/custom.py | 24 + pendulum/locales/nb/locale.py | 153 +++ pendulum/locales/nl/__init__.py | 0 pendulum/locales/nl/custom.py | 27 + pendulum/locales/nl/locale.py | 137 +++ pendulum/locales/nn/__init__.py | 0 pendulum/locales/nn/custom.py | 24 + pendulum/locales/nn/locale.py | 144 +++ pendulum/locales/pl/__init__.py | 0 pendulum/locales/pl/custom.py | 25 + pendulum/locales/pl/locale.py | 282 +++++ pendulum/locales/pt_br/__init__.py | 0 pendulum/locales/pt_br/custom.py | 22 + pendulum/locales/pt_br/locale.py | 146 +++ pendulum/locales/ru/__init__.py | 0 pendulum/locales/ru/custom.py | 24 + pendulum/locales/ru/locale.py | 273 +++++ pendulum/locales/zh/__init__.py | 0 pendulum/locales/zh/custom.py | 22 + pendulum/locales/zh/locale.py | 116 ++ pendulum/mixins/__init__.py | 1 + pendulum/mixins/default.py | 43 + pendulum/parser.py | 121 +++ pendulum/parsing/__init__.py | 234 ++++ pendulum/parsing/_iso8601.c | 1371 +++++++++++++++++++++++ pendulum/parsing/exceptions/__init__.py | 3 + pendulum/parsing/iso8601.py | 447 ++++++++ pendulum/period.py | 390 +++++++ pendulum/py.typed | 0 pendulum/time.py | 284 +++++ pendulum/tz/__init__.py | 60 + pendulum/tz/data/__init__.py | 0 pendulum/tz/data/windows.py | 137 +++ pendulum/tz/exceptions.py | 23 + pendulum/tz/local_timezone.py | 257 +++++ pendulum/tz/timezone.py | 377 +++++++ pendulum/tz/zoneinfo/__init__.py | 16 + pendulum/tz/zoneinfo/exceptions.py | 18 + pendulum/tz/zoneinfo/posix_timezone.py | 270 +++++ pendulum/tz/zoneinfo/reader.py | 224 ++++ pendulum/tz/zoneinfo/timezone.py | 128 +++ pendulum/tz/zoneinfo/transition.py | 77 ++ pendulum/tz/zoneinfo/transition_type.py | 35 + pendulum/utils/__init__.py | 0 pendulum/utils/_compat.py | 54 + pyproject.toml | 81 ++ 100 files changed, 14517 insertions(+) create mode 100644 LICENSE create mode 100644 PKG-INFO create mode 100644 README.rst create mode 100644 build.py create mode 100644 pendulum/__init__.py create mode 100644 pendulum/__version__.py create mode 100644 pendulum/_extensions/__init__.py create mode 100644 pendulum/_extensions/_helpers.c create mode 100644 pendulum/_extensions/helpers.py create mode 100644 pendulum/constants.py create mode 100644 pendulum/date.py create mode 100644 pendulum/datetime.py create mode 100644 pendulum/duration.py create mode 100644 pendulum/exceptions.py create mode 100644 pendulum/formatting/__init__.py create mode 100644 pendulum/formatting/difference_formatter.py create mode 100644 pendulum/formatting/formatter.py create mode 100644 pendulum/helpers.py create mode 100644 pendulum/locales/__init__.py create mode 100644 pendulum/locales/da/__init__.py create mode 100644 pendulum/locales/da/custom.py create mode 100644 pendulum/locales/da/locale.py create mode 100644 pendulum/locales/de/__init__.py create mode 100644 pendulum/locales/de/custom.py create mode 100644 pendulum/locales/de/locale.py create mode 100644 pendulum/locales/en/__init__.py create mode 100644 pendulum/locales/en/custom.py create mode 100644 pendulum/locales/en/locale.py create mode 100644 pendulum/locales/es/__init__.py create mode 100644 pendulum/locales/es/custom.py create mode 100644 pendulum/locales/es/locale.py create mode 100644 pendulum/locales/fa/__init__.py create mode 100644 pendulum/locales/fa/custom.py create mode 100644 pendulum/locales/fa/locale.py create mode 100644 pendulum/locales/fo/__init__.py create mode 100644 pendulum/locales/fo/custom.py create mode 100644 pendulum/locales/fo/locale.py create mode 100644 pendulum/locales/fr/__init__.py create mode 100644 pendulum/locales/fr/custom.py create mode 100644 pendulum/locales/fr/locale.py create mode 100644 pendulum/locales/id/__init__.py create mode 100644 pendulum/locales/id/custom.py create mode 100644 pendulum/locales/id/locale.py create mode 100644 pendulum/locales/it/__init__.py create mode 100644 pendulum/locales/it/custom.py create mode 100644 pendulum/locales/it/locale.py create mode 100644 pendulum/locales/ko/__init__.py create mode 100644 pendulum/locales/ko/custom.py create mode 100644 pendulum/locales/ko/locale.py create mode 100644 pendulum/locales/locale.py create mode 100644 pendulum/locales/lt/__init__.py create mode 100644 pendulum/locales/lt/custom.py create mode 100644 pendulum/locales/lt/locale.py create mode 100644 pendulum/locales/nb/__init__.py create mode 100644 pendulum/locales/nb/custom.py create mode 100644 pendulum/locales/nb/locale.py create mode 100644 pendulum/locales/nl/__init__.py create mode 100644 pendulum/locales/nl/custom.py create mode 100644 pendulum/locales/nl/locale.py create mode 100644 pendulum/locales/nn/__init__.py create mode 100644 pendulum/locales/nn/custom.py create mode 100644 pendulum/locales/nn/locale.py create mode 100644 pendulum/locales/pl/__init__.py create mode 100644 pendulum/locales/pl/custom.py create mode 100644 pendulum/locales/pl/locale.py create mode 100644 pendulum/locales/pt_br/__init__.py create mode 100644 pendulum/locales/pt_br/custom.py create mode 100644 pendulum/locales/pt_br/locale.py create mode 100644 pendulum/locales/ru/__init__.py create mode 100644 pendulum/locales/ru/custom.py create mode 100644 pendulum/locales/ru/locale.py create mode 100644 pendulum/locales/zh/__init__.py create mode 100644 pendulum/locales/zh/custom.py create mode 100644 pendulum/locales/zh/locale.py create mode 100644 pendulum/mixins/__init__.py create mode 100644 pendulum/mixins/default.py create mode 100644 pendulum/parser.py create mode 100644 pendulum/parsing/__init__.py create mode 100644 pendulum/parsing/_iso8601.c create mode 100644 pendulum/parsing/exceptions/__init__.py create mode 100644 pendulum/parsing/iso8601.py create mode 100644 pendulum/period.py create mode 100644 pendulum/py.typed create mode 100644 pendulum/time.py create mode 100644 pendulum/tz/__init__.py create mode 100644 pendulum/tz/data/__init__.py create mode 100644 pendulum/tz/data/windows.py create mode 100644 pendulum/tz/exceptions.py create mode 100644 pendulum/tz/local_timezone.py create mode 100644 pendulum/tz/timezone.py create mode 100644 pendulum/tz/zoneinfo/__init__.py create mode 100644 pendulum/tz/zoneinfo/exceptions.py create mode 100644 pendulum/tz/zoneinfo/posix_timezone.py create mode 100644 pendulum/tz/zoneinfo/reader.py create mode 100644 pendulum/tz/zoneinfo/timezone.py create mode 100644 pendulum/tz/zoneinfo/transition.py create mode 100644 pendulum/tz/zoneinfo/transition_type.py create mode 100644 pendulum/utils/__init__.py create mode 100644 pendulum/utils/_compat.py create mode 100644 pyproject.toml 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') + # + + pendulum.parse('2016-1-17') + # + + arrow.get('20160413') + # + + pendulum.parse('20160413') + # + + arrow.get('2016-W07-5') + # + + pendulum.parse('2016-W07-5') + # + + # 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 `_ +* `Documentation `_ +* `Issue Tracker `_ + + +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 `_. + +.. 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 + +It will generate a directory in ``pendulum/locales`` named after your locale, with the following +structure: + +.. code-block:: text + + / + - 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 `_ +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') + # + + pendulum.parse('2016-1-17') + # + + arrow.get('20160413') + # + + pendulum.parse('20160413') + # + + arrow.get('2016-W07-5') + # + + pendulum.parse('2016-W07-5') + # + + # 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 `_ +* `Documentation `_ +* `Issue Tracker `_ + + +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 `_. + +.. 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 + +It will generate a directory in ``pendulum/locales`` named after your locale, with the following +structure: + +.. code-block:: text + + / + - 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 `_ +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 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 +#include +#include +#include +#include +#include +#include +#include + +/* ------------------------------------------------------------------------- */ + +#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, µseconds, &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, µsecond)) + { + 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 ``_ + + :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"(? 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 diff --git a/pendulum/locales/da/__init__.py b/pendulum/locales/da/__init__.py new file mode 100644 index 0000000..e69de29 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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" + " (?P" # Classic date (YYYY-MM-DD) + r" (?P\d{4})" # Year + " (?P" + r" (?P[/:])?(?P\d{2})" # Month (optional) + r" ((?P[/:])?(?P\d{2}))" # Day (optional) + " )?" + " )" + ")?" + # Time (optional) + "(?P