summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2023-01-05 10:38:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2023-01-05 10:39:17 +0000
commitd6d80a17444c90259c5bfdacb84c61e6bfece655 (patch)
tree157bff98bd572acf0b64cd5d478b0bdac87a37ae
parentReleasing debian version 2.1.2-4. (diff)
downloadpendulum-d6d80a17444c90259c5bfdacb84c61e6bfece655.tar.xz
pendulum-d6d80a17444c90259c5bfdacb84c61e6bfece655.zip
Merging upstream version 3.0.0~a1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.flake828
-rw-r--r--.github/FUNDING.yml1
-rw-r--r--.github/ISSUE_TEMPLATE/---bug-report.md31
-rw-r--r--.github/ISSUE_TEMPLATE/---documentation.md22
-rw-r--r--.github/ISSUE_TEMPLATE/---everything-else.md19
-rw-r--r--.github/ISSUE_TEMPLATE/---feature-request.md23
-rw-r--r--.github/PULL_REQUEST_TEMPLATE.md15
-rw-r--r--.github/workflows/release.yml79
-rw-r--r--.github/workflows/tests.yml76
-rw-r--r--.gitignore37
-rw-r--r--.pre-commit-config.yaml71
-rw-r--r--CHANGELOG.md177
-rw-r--r--LICENSE40
-rw-r--r--Makefile53
-rw-r--r--PKG-INFO251
-rw-r--r--README.rst400
-rwxr-xr-xbuild-wheels.sh27
-rw-r--r--build.py120
-rwxr-xr-xclock274
-rw-r--r--codecov.yml7
-rw-r--r--docs/Makefile2
-rw-r--r--docs/docs/addition_subtraction.md87
-rw-r--r--docs/docs/attributes_properties.md87
-rw-r--r--docs/docs/comparison.md77
-rw-r--r--docs/docs/difference.md115
-rw-r--r--docs/docs/duration.md177
-rw-r--r--docs/docs/fluent_helpers.md67
-rw-r--r--docs/docs/index.md17
-rw-r--r--docs/docs/installation.md13
-rw-r--r--docs/docs/instantiation.md144
-rw-r--r--docs/docs/introduction.md21
-rw-r--r--docs/docs/limitations.md43
-rw-r--r--docs/docs/localization.md36
-rw-r--r--docs/docs/modifiers.md86
-rw-r--r--docs/docs/parsing.md114
-rw-r--r--docs/docs/period.md168
-rw-r--r--docs/docs/string_formatting.md175
-rw-r--r--docs/docs/testing.md59
-rw-r--r--docs/docs/timezones.md193
-rw-r--r--docs/mkdocs.yml17
-rw-r--r--docs/theme/main.html33
-rw-r--r--meson.build20
-rw-r--r--pendulum/__init__.py678
-rw-r--r--pendulum/__version__.py2
-rw-r--r--pendulum/_extensions/_helpers.c1861
-rw-r--r--pendulum/_extensions/_helpers.pyi31
-rw-r--r--pendulum/_extensions/helpers.py722
-rw-r--r--pendulum/constants.py216
-rw-r--r--pendulum/date.py1651
-rw-r--r--pendulum/datetime.py2944
-rw-r--r--pendulum/duration.py981
-rw-r--r--pendulum/exceptions.py14
-rw-r--r--pendulum/formatting/__init__.py9
-rw-r--r--pendulum/formatting/difference_formatter.py299
-rw-r--r--pendulum/formatting/formatter.py1368
-rw-r--r--pendulum/helpers.py447
-rw-r--r--pendulum/interval.py448
-rw-r--r--pendulum/locales/cs/__init__.py0
-rw-r--r--pendulum/locales/cs/custom.py23
-rw-r--r--pendulum/locales/cs/locale.py266
-rw-r--r--pendulum/locales/da/custom.py40
-rw-r--r--pendulum/locales/da/locale.py297
-rw-r--r--pendulum/locales/de/custom.py76
-rw-r--r--pendulum/locales/de/locale.py291
-rw-r--r--pendulum/locales/en/custom.py50
-rw-r--r--pendulum/locales/en/locale.py303
-rw-r--r--pendulum/locales/es/custom.py50
-rw-r--r--pendulum/locales/es/locale.py285
-rw-r--r--pendulum/locales/fa/custom.py40
-rw-r--r--pendulum/locales/fa/locale.py273
-rw-r--r--pendulum/locales/fo/custom.py44
-rw-r--r--pendulum/locales/fo/locale.py267
-rw-r--r--pendulum/locales/fr/__init__.py1
-rw-r--r--pendulum/locales/fr/custom.py50
-rw-r--r--pendulum/locales/fr/locale.py269
-rw-r--r--pendulum/locales/he/__init__.py0
-rw-r--r--pendulum/locales/he/custom.py23
-rw-r--r--pendulum/locales/he/locale.py269
-rw-r--r--pendulum/locales/id/custom.py42
-rw-r--r--pendulum/locales/id/locale.py285
-rw-r--r--pendulum/locales/it/custom.py51
-rw-r--r--pendulum/locales/it/locale.py293
-rw-r--r--pendulum/locales/ja/__init__.py0
-rw-r--r--pendulum/locales/ja/custom.py21
-rw-r--r--pendulum/locales/ja/locale.py194
-rw-r--r--pendulum/locales/ko/custom.py40
-rw-r--r--pendulum/locales/ko/locale.py213
-rw-r--r--pendulum/locales/locale.py206
-rw-r--r--pendulum/locales/lt/custom.py240
-rw-r--r--pendulum/locales/lt/locale.py513
-rw-r--r--pendulum/locales/nb/custom.py44
-rw-r--r--pendulum/locales/nb/locale.py303
-rw-r--r--pendulum/locales/nl/custom.py50
-rw-r--r--pendulum/locales/nl/locale.py271
-rw-r--r--pendulum/locales/nn/custom.py44
-rw-r--r--pendulum/locales/nn/locale.py285
-rw-r--r--pendulum/locales/pl/custom.py46
-rw-r--r--pendulum/locales/pl/locale.py561
-rw-r--r--pendulum/locales/pt_br/custom.py40
-rw-r--r--pendulum/locales/pt_br/locale.py289
-rw-r--r--pendulum/locales/ru/custom.py44
-rw-r--r--pendulum/locales/ru/locale.py543
-rw-r--r--pendulum/locales/sk/__init__.py0
-rw-r--r--pendulum/locales/sk/custom.py20
-rw-r--r--pendulum/locales/sk/locale.py266
-rw-r--r--pendulum/locales/sv/__init__.py0
-rw-r--r--pendulum/locales/sv/custom.py20
-rw-r--r--pendulum/locales/sv/locale.py222
-rw-r--r--pendulum/locales/zh/custom.py40
-rw-r--r--pendulum/locales/zh/locale.py229
-rw-r--r--pendulum/mixins/__init__.py1
-rw-r--r--pendulum/mixins/default.py79
-rw-r--r--pendulum/parser.py245
-rw-r--r--pendulum/parsing/__init__.py467
-rw-r--r--pendulum/parsing/_iso8601.c2732
-rw-r--r--pendulum/parsing/_iso8601.pyi22
-rw-r--r--pendulum/parsing/exceptions/__init__.py9
-rw-r--r--pendulum/parsing/iso8601.py901
-rw-r--r--pendulum/period.py390
-rw-r--r--pendulum/testing/__init__.py0
-rw-r--r--pendulum/testing/traveller.py139
-rw-r--r--pendulum/time.py587
-rw-r--r--pendulum/tz/__init__.py140
-rw-r--r--pendulum/tz/data/windows.py276
-rw-r--r--pendulum/tz/exceptions.py55
-rw-r--r--pendulum/tz/local_timezone.py517
-rw-r--r--pendulum/tz/timezone.py594
-rw-r--r--pendulum/tz/zoneinfo/__init__.py16
-rw-r--r--pendulum/tz/zoneinfo/exceptions.py18
-rw-r--r--pendulum/tz/zoneinfo/posix_timezone.py270
-rw-r--r--pendulum/tz/zoneinfo/reader.py224
-rw-r--r--pendulum/tz/zoneinfo/timezone.py128
-rw-r--r--pendulum/tz/zoneinfo/transition.py77
-rw-r--r--pendulum/tz/zoneinfo/transition_type.py35
-rw-r--r--pendulum/utils/_compat.py67
-rw-r--r--poetry.lock1172
-rw-r--r--pyproject.toml264
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/conftest.py101
-rw-r--r--tests/date/__init__.py0
-rw-r--r--tests/date/test_add.py88
-rw-r--r--tests/date/test_behavior.py73
-rw-r--r--tests/date/test_comparison.py247
-rw-r--r--tests/date/test_construct.py16
-rw-r--r--tests/date/test_day_of_week_modifiers.py298
-rw-r--r--tests/date/test_diff.py365
-rw-r--r--tests/date/test_fluent_setters.py29
-rw-r--r--tests/date/test_getters.py87
-rw-r--r--tests/date/test_start_end_of.py252
-rw-r--r--tests/date/test_strings.py48
-rw-r--r--tests/date/test_sub.py90
-rw-r--r--tests/datetime/__init__.py0
-rw-r--r--tests/datetime/test_add.py268
-rw-r--r--tests/datetime/test_behavior.py172
-rw-r--r--tests/datetime/test_comparison.py394
-rw-r--r--tests/datetime/test_construct.py182
-rw-r--r--tests/datetime/test_create_from_timestamp.py24
-rw-r--r--tests/datetime/test_day_of_week_modifiers.py314
-rw-r--r--tests/datetime/test_diff.py880
-rw-r--r--tests/datetime/test_fluent_setters.py181
-rw-r--r--tests/datetime/test_from_format.py203
-rw-r--r--tests/datetime/test_getters.py248
-rw-r--r--tests/datetime/test_naive.py78
-rw-r--r--tests/datetime/test_replace.py61
-rw-r--r--tests/datetime/test_start_end_of.py285
-rw-r--r--tests/datetime/test_strings.py141
-rw-r--r--tests/datetime/test_sub.py251
-rw-r--r--tests/datetime/test_timezone.py38
-rw-r--r--tests/duration/__init__.py0
-rw-r--r--tests/duration/test_add_sub.py54
-rw-r--r--tests/duration/test_arithmetic.py85
-rw-r--r--tests/duration/test_behavior.py21
-rw-r--r--tests/duration/test_construct.py99
-rw-r--r--tests/duration/test_in_methods.py28
-rw-r--r--tests/duration/test_in_words.py77
-rw-r--r--tests/duration/test_total_methods.py28
-rw-r--r--tests/fixtures/__init__.py0
-rw-r--r--tests/fixtures/tz/Parisbin0 -> 2971 bytes
-rw-r--r--tests/fixtures/tz/clock/etc/sysconfig/clock1
l---------tests/fixtures/tz/symlink/etc/localtime1
-rw-r--r--tests/fixtures/tz/symlink/usr/share/zoneinfo/Europe/Parisbin0 -> 2971 bytes
l---------tests/fixtures/tz/timezone_dir/etc/localtime1
-rw-r--r--tests/fixtures/tz/timezone_dir/etc/timezone/blank.md5
-rw-r--r--tests/fixtures/tz/timezone_dir/usr/share/zoneinfo/Europe/Parisbin0 -> 2971 bytes
-rw-r--r--tests/formatting/__init__.py0
-rw-r--r--tests/formatting/test_formatter.py263
-rw-r--r--tests/helpers/__init__.py0
-rw-r--r--tests/helpers/test_local_time.py31
-rw-r--r--tests/interval/__init__.py0
-rw-r--r--tests/interval/test_add_subtract.py49
-rw-r--r--tests/interval/test_arithmetic.py53
-rw-r--r--tests/interval/test_behavior.py54
-rw-r--r--tests/interval/test_construct.py121
-rw-r--r--tests/interval/test_hashing.py23
-rw-r--r--tests/interval/test_in_words.py70
-rw-r--r--tests/interval/test_range.py119
-rw-r--r--tests/localization/__init__.py0
-rw-r--r--tests/localization/test_cs.py109
-rw-r--r--tests/localization/test_da.py65
-rw-r--r--tests/localization/test_de.py65
-rw-r--r--tests/localization/test_es.py65
-rw-r--r--tests/localization/test_fa.py65
-rw-r--r--tests/localization/test_fo.py65
-rw-r--r--tests/localization/test_fr.py84
-rw-r--r--tests/localization/test_he.py65
-rw-r--r--tests/localization/test_id.py68
-rw-r--r--tests/localization/test_it.py85
-rw-r--r--tests/localization/test_ja.py68
-rw-r--r--tests/localization/test_ko.py65
-rw-r--r--tests/localization/test_lt.py68
-rw-r--r--tests/localization/test_nb.py84
-rw-r--r--tests/localization/test_nl.py83
-rw-r--r--tests/localization/test_nn.py84
-rw-r--r--tests/localization/test_pl.py109
-rw-r--r--tests/localization/test_ru.py86
-rw-r--r--tests/localization/test_sk.py112
-rw-r--r--tests/localization/test_sv.py86
-rw-r--r--tests/parsing/__init__.py0
-rw-r--r--tests/parsing/test_parse_iso8601.py465
-rw-r--r--tests/parsing/test_parsing.py687
-rw-r--r--tests/parsing/test_parsing_duration.py298
-rw-r--r--tests/test_helpers.py179
-rw-r--r--tests/test_main.py13
-rw-r--r--tests/test_parsing.py141
-rw-r--r--tests/testing/__init__.py0
-rw-r--r--tests/testing/test_time_travel.py85
-rw-r--r--tests/time/__init__.py0
-rw-r--r--tests/time/test_add.py78
-rw-r--r--tests/time/test_behavior.py49
-rw-r--r--tests/time/test_comparison.py185
-rw-r--r--tests/time/test_construct.py22
-rw-r--r--tests/time/test_diff.py350
-rw-r--r--tests/time/test_fluent_setters.py12
-rw-r--r--tests/time/test_strings.py39
-rw-r--r--tests/time/test_sub.py112
-rw-r--r--tests/tz/__init__.py0
-rw-r--r--tests/tz/test_helpers.py27
-rw-r--r--tests/tz/test_local_timezone.py52
-rw-r--r--tests/tz/test_timezone.py447
-rw-r--r--tests/tz/test_timezones.py16
-rw-r--r--tox.ini18
241 files changed, 29866 insertions, 14517 deletions
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..d571285
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,28 @@
+[flake8]
+max-line-length = 88
+per-file-ignores =
+ # F401: Module imported but unused
+ # TC001: Move import into type checking block
+ __init__.py:F401, TC001
+ # F811: Redefinition of unused name from line n
+ pendulum/tz/timezone.py:F811
+min_python_version = 3.7.0
+ban-relative-imports = true
+# flake8-use-fstring: https://github.com/MichaelKim0407/flake8-use-fstring#--percent-greedy-and---format-greedy
+format-greedy = 1
+inline-quotes = double
+enable-extensions = TC, TC1
+type-checking-exempt-modules = typing, typing-extensions
+eradicate-whitelist-extend = ^-.*;
+extend-ignore =
+ # E501: Line too long
+ E501,
+ # E203: Whitespace before ':'
+ E203,
+ # SIM106: Handle error-cases first
+ SIM106,
+extend-exclude =
+ # External to the project's coding standards:
+ docs/*,
+ # Machine-generated, too many false-positives
+ pendulum/locales/*,
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..fca881b
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: [sdispater]
diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md
new file mode 100644
index 0000000..1f8b618
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/---bug-report.md
@@ -0,0 +1,31 @@
+---
+name: "\U0001F41E Bug Report"
+about: Did you find a bug?
+title: ''
+labels: 'Bug'
+assignees: ''
+
+---
+
+<!--
+ Hi there! Thank you for discovering and submitting an issue.
+
+ Before you submit this; let's make sure of a few things.
+ Please make sure the following boxes are ticked if they are correct.
+ If not, please try and fulfill these first.
+-->
+
+<!-- Checked checkbox should look like this: [x] -->
+- [ ] I am on the [latest](https://github.com/sdispater/pendulum/releases/latest) Pendulum version.
+- [ ] I have searched the [issues](https://github.com/sdispater/pendulum/issues) of this repo and believe that this is not a duplicate.
+
+<!--
+ Once those are done, if you're able to fill in the following list with your information,
+ it'd be very helpful to whoever handles the issue.
+-->
+
+- **OS version and name**: <!-- Replace with version + name -->
+- **Pendulum version**: <!-- Replace with version -->
+
+## Issue
+<!-- Now feel free to write your issue, but please be descriptive! Thanks again 🙌 ❤️ -->
diff --git a/.github/ISSUE_TEMPLATE/---documentation.md b/.github/ISSUE_TEMPLATE/---documentation.md
new file mode 100644
index 0000000..0d4ac41
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/---documentation.md
@@ -0,0 +1,22 @@
+---
+name: "\U0001F4DA Documentation"
+about: Did you find errors, problems, or anything unintelligible in the docs (https://pendulum.eustace.io/docs)?
+title: ''
+labels: 'Documentation'
+assignees: ''
+
+---
+
+<!--
+ Hi there! Thank you for discovering and submitting an issue with our documentation.
+
+ Before you submit this; let's make sure of a few things.
+ Please make sure the following boxes are ticked if they are correct.
+ If not, please try and fulfill these first.
+-->
+
+<!-- Checked checkbox should look like this: [x] -->
+- [ ] I have searched the [issues](https://github.com/sdispater/pendulum/issues) of this repo and believe that this is not a duplicate.
+
+## Issue
+<!-- Now feel free to write your issue, but please be descriptive! Thanks again 🙌 ❤️ -->
diff --git a/.github/ISSUE_TEMPLATE/---everything-else.md b/.github/ISSUE_TEMPLATE/---everything-else.md
new file mode 100644
index 0000000..1fc60fa
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/---everything-else.md
@@ -0,0 +1,19 @@
+---
+name: "\U0001F5C3 Everything Else"
+about: For questions and issues that do not fall in any of the other categories. This
+ can include questions about Pendulum's roadmap.
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+<!-- Describe your question and issue here. This space is meant to be used for general questions that are neither bugs, feature requests, nor documentation issues. A good example would be a question regarding Pendulum's roadmap, for example.
+
+
+<!-- Checked checkbox should look like this: [x] -->
+- [ ] I have searched the [issues](https://github.com/sdispater/pendulum/issues) of this repo and believe that this is not a duplicate.
+- [ ] I have searched the [documentation](https://pendulum.eustace.io/docs/) and believe that my question is not covered.
+
+## Issue
+<!-- Now feel free to write your issue, but please be descriptive! Thanks again 🙌 ❤️ -->
diff --git a/.github/ISSUE_TEMPLATE/---feature-request.md b/.github/ISSUE_TEMPLATE/---feature-request.md
new file mode 100644
index 0000000..4605055
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/---feature-request.md
@@ -0,0 +1,23 @@
+---
+name: "\U0001F381 Feature Request"
+about: Do you have ideas for new features and improvements?
+title: ''
+labels: 'Feature'
+assignees: ''
+
+---
+
+<!--
+ Hi there! Thank you for wanting to make Pendulum better.
+
+ Before you submit this; let's make sure of a few things.
+ Please make sure the following boxes are ticked if they are correct.
+ If not, please try and fulfill these first.
+-->
+
+<!-- Checked checkbox should look like this: [x] -->
+- [ ] I have searched the [issues](https://github.com/sdispater/pendulum/issues) of this repo and believe that this is not a duplicate.
+- [ ] I have searched the [documentation](https://pendulum.eustace.io/docs/) and believe that my question is not covered.
+
+## Feature Request
+<!-- Now feel free to write your idea for improvement. Thanks again 🙌 ❤️ -->
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..4c0ce41
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,15 @@
+## Pull Request Check List
+
+<!--
+This is just a reminder about the most common mistakes. Please make sure that you tick all *appropriate* boxes. But please read our [contribution guide](https://python-poetry.org/docs/contributing/) at least once, it will save you unnecessary review cycles!
+-->
+
+- [ ] Added **tests** for changed code.
+- [ ] Updated **documentation** for changed code.
+
+<!--
+**Note**: If your Pull Request introduces a new feature or changes the current behavior, it should be based
+on the `develop` branch. If it's a bug fix or only a documentation update, it should be based on the `master` branch.
+
+If you have *any* questions to *any* of the points above, just **submit and ask**! This checklist is here to *help* you, not to deter you from contributing!
+-->
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..59062ae
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,79 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - '*.*.*'
+
+jobs:
+
+ build:
+ name: Build wheels on ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}-latest
+ strategy:
+ matrix:
+ os: [ ubuntu, windows, macos ]
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Build wheels
+ uses: pypa/cibuildwheel@v2.10.1
+ env:
+ CIBW_PROJECT_REQUIRES_PYTHON: ">=3.7"
+ with:
+ package-dir: .
+ output-dir: dist
+
+ - uses: actions/upload-artifact@v3
+ with:
+ name: dist
+ path: ./dist/*
+
+ Release:
+ needs: [ build ]
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Download artifacts
+ uses: actions/download-artifact@v3
+ with:
+ name: dist
+ path: dist
+
+ - name: Install Poetry
+ run: |
+ curl -fsS https://install.python-poetry.org | python - -y
+
+ - name: Update PATH
+ run: echo "$HOME/.local/bin" >> $GITHUB_PATH
+
+ - name: Build sdist
+ run: poetry build --format sdist
+
+ - name: Check distributions
+ run: |
+ ls -la dist
+
+ - name: Check Version
+ id: check-version
+ run: |
+ [[ "${GITHUB_REF#refs/tags/}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
+ || echo ::set-output name=prerelease::true
+
+ - name: Create Release
+ uses: ncipollo/release-action@v1
+ with:
+ artifacts: "dist/*"
+ token: ${{ secrets.GITHUB_TOKEN }}
+ draft: false
+ prerelease: steps.check-version.outputs.prerelease == 'true'
+
+ - name: Publish to PyPI
+ env:
+ POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }}
+ run: |
+ poetry publish
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..341859e
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,76 @@
+name: Tests
+
+on:
+ push:
+ paths-ignore:
+ - 'docs/**'
+ branches:
+ - master
+ pull_request:
+ paths-ignore:
+ - 'docs/**'
+ branches:
+ - '**'
+
+jobs:
+ Tests:
+ name: ${{ matrix.os }} / ${{ matrix.python-version }}
+ runs-on: ${{ matrix.os }}-latest
+ strategy:
+ matrix:
+ os: [Ubuntu, MacOS, Windows]
+ python-version: [3.7, 3.8, 3.9, "3.10"]
+ defaults:
+ run:
+ shell: bash
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Get full Python version
+ id: full-python-version
+ run: |
+ echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))")
+
+ - name: Install poetry
+ run: |
+ curl -fsS https://install.python-poetry.org | python - --preview -y
+
+ - name: Update PATH
+ if: ${{ matrix.os != 'Windows' }}
+ run: echo "$HOME/.local/bin" >> $GITHUB_PATH
+
+ - name: Update Path for Windows
+ if: ${{ matrix.os == 'Windows' }}
+ run: echo "$APPDATA\Python\Scripts" >> $GITHUB_PATH
+
+ - name: Configure poetry
+ run: poetry config virtualenvs.in-project true
+
+ - name: Set up cache
+ uses: actions/cache@v3
+ id: cache
+ with:
+ path: .venv
+ key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }}
+
+ - name: Ensure cache is healthy
+ # MacOS does not come with `timeout` command out of the box
+ if: steps.cache.outputs.cache-hit == 'true' && matrix.os != 'MacOS'
+ run: timeout 10s poetry run pip --version || rm -rf .venv
+
+ - name: Install dependencies
+ run: poetry install --only main --only test -vvv
+
+ - name: Test Pure Python
+ run: |
+ PENDULUM_EXTENSIONS=0 poetry run pytest -q tests
+
+ - name: Test
+ run: |
+ poetry run pytest -q tests
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bb25f8b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,37 @@
+*.pyc
+
+# Packages
+*.egg
+*.egg-info
+dist
+build
+_build
+.cache
+*.so
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+nosetests.xml
+.pytest_cache
+
+.DS_Store
+.idea/*
+.python-version
+
+/test.py
+/test_*.*
+/benchmark/*
+benchmark.py
+results.json
+profile.html
+/wheelhouse
+/docs/site/*
+setup.py
+
+# editor
+
+.vscode
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..0e8d094
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,71 @@
+ci:
+ autofix_prs: false
+
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.3.0
+ hooks:
+ - id: trailing-whitespace
+ exclude: ^tests/.*/fixtures/.*
+ - id: end-of-file-fixer
+ exclude: ^tests/.*/fixtures/.*
+ - id: debug-statements
+
+ - repo: https://github.com/hadialqattan/pycln
+ rev: v2.1.1
+ hooks:
+ - id: pycln
+ args: [ --all ]
+
+ - repo: https://github.com/psf/black
+ rev: 22.10.0
+ hooks:
+ - id: black
+
+ - repo: https://github.com/pycqa/isort
+ rev: 5.10.1
+ hooks:
+ - id: isort
+ args: [ --add-import, from __future__ import annotations, --lines-after-imports, "-1" ]
+
+ - repo: https://github.com/pycqa/flake8
+ rev: 5.0.4
+ hooks:
+ - id: flake8
+ additional_dependencies: &flake8_deps
+ - flake8-broken-line==0.5.0
+ - flake8-bugbear==22.7.1
+ - flake8-comprehensions==3.10.0
+ - flake8-eradicate==1.3.0
+ - flake8-quotes==3.3.1
+ - flake8-simplify==0.19.2
+ - flake8-tidy-imports==4.8.0
+ - flake8-type-checking==2.1.2
+ - flake8-typing-imports==1.12.0
+ - flake8-use-fstring==1.3
+ - pep8-naming==0.13.1
+
+ - repo: https://github.com/asottile/yesqa
+ rev: v1.4.0
+ hooks:
+ - id: yesqa
+ additional_dependencies: *flake8_deps
+
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v3.2.0
+ hooks:
+ - id: pyupgrade
+ exclude: ^build\.py$
+ args:
+ - --py37-plus
+
+ - repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v0.982
+ hooks:
+ - id: mypy
+ pass_filenames: false
+ exclude: ^build\.py$
+ additional_dependencies:
+ - pytest>=7.1.2
+ - types-backports
+ - types-python-dateutil
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..ae71991
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,177 @@
+# Change Log
+
+## [3.0.0a1] - 2022-11-23
+
+### Added
+
+- Added new testing helpers to time travel. [#626](https://github.com/sdispater/pendulum/pull/626)
+
+### Changed
+
+- Dropped support for Python 2.7, 3.5 and 3.6. [#569](https://github.com/sdispater/pendulum/pull/569)
+- The `Timezone` class now relies on the native `zoneinfo.ZoneInfo` class. [#569](https://github.com/sdispater/pendulum/pull/569)
+- Renamed the `Period` class to `Interval`. [#676](https://github.com/sdispater/pendulum/pull/676)
+- Renamed the `period` helper to `interval`. [#676](https://github.com/sdispater/pendulum/pull/676)
+- Removed existing testing helpers: `test()` and `set_test_now()`. [#626](https://github.com/sdispater/pendulum/pull/626)
+
+### Locales
+
+- Added the `sk` locale. [#575](https://github.com/sdispater/pendulum/pull/575)
+- Added the `ja` locale. [#610](https://github.com/sdispater/pendulum/pull/610)
+- Added the `he` locale. [#585](https://github.com/sdispater/pendulum/pull/585)
+- Added the `sv` locale. [#562](https://github.com/sdispater/pendulum/pull/562)
+
+
+## [2.1.1] - 2020-07-13
+
+### Fixed
+
+- Fixed errors where invalid timezones were matched in `from_format()` ([#374](https://github.com/sdispater/pendulum/pull/374)).
+- Fixed errors when subtracting negative timedeltas ([#419](https://github.com/sdispater/pendulum/pull/419)).
+- Fixed errors in total units computation for durations with years and months ([#482](https://github.com/sdispater/pendulum/pull/482)).
+- Fixed an error where the `fold` attribute was overridden when using `replace()` ([#414](https://github.com/sdispater/pendulum/pull/414)).
+- Fixed an error where `now()` was not returning the correct result on DST transitions ([#483](https://github.com/sdispater/pendulum/pull/483)).
+- Fixed inconsistent typing annotation for the `parse()` function ([#452](https://github.com/sdispater/pendulum/pull/452)).
+
+### Locales
+
+- Added the `pl` locale ([#459](https://github.com/sdispater/pendulum/pull/459)).
+
+
+## [2.1.0] - 2020-03-07
+
+### Added
+
+- Added better typing and PEP-561 compliance ([#320](https://github.com/sdispater/pendulum/pull/320)).
+- Added the `is_anniversary()` method as an alias of `is_birthday()` ([#298](https://github.com/sdispater/pendulum/pull/298)).
+
+### Changed
+
+- Dropped support for Python 3.4.
+- `is_utc()` will now return `True` for any datetime with an offset of 0, similar to the behavior in the `1.*` versions ([#295](https://github.com/sdispater/pendulum/pull/295))
+- `Duration.in_words()` will now return `0 milliseconds` for empty durations.
+
+### Fixed
+
+- Fixed various issues with timezone transitions for some edge cases ([#321](https://github.com/sdispater/pendulum/pull/321), ([#350](https://github.com/sdispater/pendulum/pull/350))).
+- Fixed out of bound detection for `nth_of("month")` ([#357](https://github.com/sdispater/pendulum/pull/357)).
+- Fixed an error where extra text was accepted in `from_format()` ([#372](https://github.com/sdispater/pendulum/pull/372)).
+- Fixed a recursion error when adding time to a `DateTime` with a fixed timezone ([#431](https://github.com/sdispater/pendulum/pull/431)).
+- Fixed errors where `Period` instances were not properly compared to other classes, especially `timedelta` instances ([#427](https://github.com/sdispater/pendulum/pull/427)).
+- Fixed deprecation warnings due to internal regexps ([#427](https://github.com/sdispater/pendulum/pull/427)).
+- Fixed an error where the `test()` helper would not unset the test instance when an exception was raised ([#445](https://github.com/sdispater/pendulum/pull/445)).
+- Fixed an error where the `week_of_month` attribute was not returning the correct value ([#446](https://github.com/sdispater/pendulum/pull/446)).
+- Fixed an error in the way the `Z` ISO-8601 UTC designator was not parsed as UTC ([#448](https://github.com/sdispater/pendulum/pull/448)).
+
+### Locales
+
+- Added the `nl` locale.
+- Added the `it` locale.
+- Added the `id` locale.
+- Added the `nb` locale.
+- Added the `nn` locale.
+
+
+## [2.0.5] - 2019-07-03
+
+### Fixed
+
+- Fixed ISO week dates not being parsed properly in `from_format()`.
+- Fixed loading of some timezones with empty posix spec.
+- Fixed deprecation warnings.
+
+### Locales
+
+- Added RU locale.
+
+
+## [2.0.4] - 2018-10-30
+
+### Fixed
+
+- Fixed `from_format()` not recognizing input strings when the specified pattern had escaped elements.
+- Fixed missing `x` token for string formatting.
+- Fixed reading timezone files.
+- Added support for parsing padded 2-digit days of the month with `from_format()`
+- Fixed `from_format()` trying to parse escaped tokens.
+- Fixed the `z` token timezone parsing in `from_format()` to allow underscores.
+- Fixed C extensions build errors.
+- Fixed `age` calculation for future dates.
+
+
+## [2.0.3] - 2018-07-30
+
+### Fixed
+
+- Fixed handling of `pytz` timezones.
+- Fixed some formatter's tokens handling.
+- Fixed errors on some systems when retrieving timezone from localtime files.
+- Fixed `diff` methods.
+- Fixed `closest()/farthest()` methods.
+
+
+## [2.0.2] - 2018-05-29
+
+### Fixed
+
+- Fixed the `weeks` property for negative `Period` instances.
+- Fixed `start_of()` methods not setting microseconds to 0.
+- Fixed errors on some systems when retrieving timezone from clock files.
+- Fixed parsing of partial time.
+- Fixed parsing not raising an error for week 53 for ordinary years.
+- Fixed string formatting not supporting `strftime` format.
+
+
+## [2.0.1] - 2018-05-10
+
+### Fixed
+
+- Fixed behavior of the `YY` token in `from_format()`.
+- Fixed errors on some systems when retrieving timezone from clock files.
+
+
+## [2.0.0] - 2018-05-08
+
+### Added
+
+- Added years and months support to durations.
+- Added the `test_local_timezone()` and `set_local_timezone()` helpers to ease testing.
+- Added support of ISO 8601 duration parsing.
+- Added support of ISO 8601 interval parsing.
+- Added a `local()` helper.
+- Added a `naive()` helper and a `naive()` method.
+- Added support for POSIX specification to extend timezones DST transitions.
+
+### Changed
+
+- `Pendulum` class has been renamed to `DateTime`.
+- `Interval` class has been renamed to `Duration`.
+- Changed and improved the timezone system.
+- Removed the `create()` helper.
+- Removed the `utcnow()` helper.
+- `strict` keyword argument for `parse` has been renamed to `exact`.
+- `at()` now supports setting partial time.
+- `local`, `utc` and `is_dst` are now methods rather than properties (`is_local()`, `is_utc()`, `is_dst()`).
+- Changed the `repr` of most common objects.
+- Made the `strict` keyword argument for `parse` false by default, which means it will not fallback on the `dateutil` parser.
+- Improved performances of the `precise_diff()` helper.
+- The `alternative` formatter is now the default one.
+- `set_to_string_format()/reset_to_string_format()` methods have been removed.
+- `from_format()` now uses the alternative formatter tokens.
+- Removed `xrange()` method of the `Period` class and made `range()` a generator.
+- New locale system which uses CLDR data for most of the translations.
+- `diff_for_humans()` now returns `a few seconds` where appropriate.
+- Removed `Period.intersect()`.
+
+
+
+[Unreleased]: https://github.com/sdispater/pendulum/compare/3.0.0a1...master
+[3.0.0a1]: https://github.com/sdispater/pendulum/releases/tag/3.0.0a1
+[2.1.1]: https://github.com/sdispater/pendulum/releases/tag/2.1.1
+[2.1.0]: https://github.com/sdispater/pendulum/releases/tag/2.1.0
+[2.0.5]: https://github.com/sdispater/pendulum/releases/tag/2.0.5
+[2.0.4]: https://github.com/sdispater/pendulum/releases/tag/2.0.4
+[2.0.3]: https://github.com/sdispater/pendulum/releases/tag/2.0.3
+[2.0.2]: https://github.com/sdispater/pendulum/releases/tag/2.0.2
+[2.0.1]: https://github.com/sdispater/pendulum/releases/tag/2.0.1
+[2.0.0]: https://github.com/sdispater/pendulum/releases/tag/2.0.0
diff --git a/LICENSE b/LICENSE
index b9cd466..701df22 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,20 +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.
+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/Makefile b/Makefile
new file mode 100644
index 0000000..e68b94c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,53 @@
+# This file is part of orator
+# https://github.com/sdispater/orator
+
+# Licensed under the MIT license:
+# http://www.opensource.org/licenses/MIT-license
+# Copyright (c) 2015 Sébastien Eustace
+
+PENDULUM_RELEASE := $$(sed -n -E "s/VERSION = \"(.+)\"/\1/p" pendulum/version.py)
+
+# lists all available targets
+list:
+ @sh -c "$(MAKE) -p no_targets__ | \
+ awk -F':' '/^[a-zA-Z0-9][^\$$#\/\\t=]*:([^=]|$$)/ {\
+ split(\$$1,A,/ /);for(i in A)print A[i]\
+ }' | grep -v '__\$$' | grep -v 'make\[1\]' | grep -v 'Makefile' | sort"
+# required for list
+no_targets__:
+
+# install all dependencies
+setup: setup-python
+
+# test your application (tests in the tests/ directory)
+test:
+ @py.test --cov=pendulum --cov-config .coveragerc tests/ -sq
+
+linux_release: wheels_x64 wheels_i686
+
+release: wheels_x64 wheels_i686 wheel
+
+publish:
+ @poetry publish --no-build
+
+tar:
+ python setup.py sdist --formats=gztar
+
+wheel:
+ @poetry build -v
+
+wheels_x64: build_wheels_x64
+
+wheels_i686: build_wheels_i686
+
+build_wheels_x64:
+ docker pull quay.io/pypa/manylinux1_x86_64
+ docker run --rm -v `pwd`:/io quay.io/pypa/manylinux1_x86_64 /io/build-wheels.sh
+
+build_wheels_i686:
+ docker pull quay.io/pypa/manylinux1_i686
+ docker run --rm -v `pwd`:/io quay.io/pypa/manylinux1_i686 /io/build-wheels.sh
+
+# run tests against all supported python versions
+tox:
+ @tox
diff --git a/PKG-INFO b/PKG-INFO
deleted file mode 100644
index ff329a6..0000000
--- a/PKG-INFO
+++ /dev/null
@@ -1,251 +0,0 @@
-Metadata-Version: 2.1
-Name: pendulum
-Version: 2.1.2
-Summary: Python datetimes made easy
-Home-page: https://pendulum.eustace.io
-License: MIT
-Keywords: datetime,date,time
-Author: Sébastien Eustace
-Author-email: sebastien@eustace.io
-Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
-Classifier: License :: OSI Approved :: MIT License
-Classifier: Programming Language :: Python :: 2
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Requires-Dist: python-dateutil (>=2.6,<3.0)
-Requires-Dist: pytzdata (>=2020.1)
-Requires-Dist: typing (>=3.6,<4.0); python_version < "3.5"
-Project-URL: Documentation, https://pendulum.eustace.io/docs
-Project-URL: Repository, https://github.com/sdispater/pendulum
-Description-Content-Type: text/x-rst
-
-Pendulum
-########
-
-.. image:: https://img.shields.io/pypi/v/pendulum.svg
- :target: https://pypi.python.org/pypi/pendulum
-
-.. image:: https://img.shields.io/pypi/l/pendulum.svg
- :target: https://pypi.python.org/pypi/pendulum
-
-.. image:: https://img.shields.io/codecov/c/github/sdispater/pendulum/master.svg
- :target: https://codecov.io/gh/sdispater/pendulum/branch/master
-
-.. image:: https://travis-ci.org/sdispater/pendulum.svg
- :alt: Pendulum Build status
- :target: https://travis-ci.org/sdispater/pendulum
-
-Python datetimes made easy.
-
-Supports Python **2.7** and **3.4+**.
-
-
-.. code-block:: python
-
- >>> import pendulum
-
- >>> now_in_paris = pendulum.now('Europe/Paris')
- >>> now_in_paris
- '2016-07-04T00:49:58.502116+02:00'
-
- # Seamless timezone switching
- >>> now_in_paris.in_timezone('UTC')
- '2016-07-03T22:49:58.502116+00:00'
-
- >>> tomorrow = pendulum.now().add(days=1)
- >>> last_week = pendulum.now().subtract(weeks=1)
-
- >>> past = pendulum.now().subtract(minutes=2)
- >>> past.diff_for_humans()
- >>> '2 minutes ago'
-
- >>> delta = past - last_week
- >>> delta.hours
- 23
- >>> delta.in_words(locale='en')
- '6 days 23 hours 58 minutes'
-
- # Proper handling of datetime normalization
- >>> pendulum.datetime(2013, 3, 31, 2, 30, tz='Europe/Paris')
- '2013-03-31T03:30:00+02:00' # 2:30 does not exist (Skipped time)
-
- # Proper handling of dst transitions
- >>> just_before = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, tz='Europe/Paris')
- '2013-03-31T01:59:59.999999+01:00'
- >>> just_before.add(microseconds=1)
- '2013-03-31T03:00:00+02:00'
-
-
-Why Pendulum?
-=============
-
-Native ``datetime`` instances are enough for basic cases but when you face more complex use-cases
-they often show limitations and are not so intuitive to work with.
-``Pendulum`` provides a cleaner and more easy to use API while still relying on the standard library.
-So it's still ``datetime`` but better.
-
-Unlike other datetime libraries for Python, Pendulum is a drop-in replacement
-for the standard ``datetime`` class (it inherits from it), so, basically, you can replace all your ``datetime``
-instances by ``DateTime`` instances in you code (exceptions exist for libraries that check
-the type of the objects by using the ``type`` function like ``sqlite3`` or ``PyMySQL`` for instance).
-
-It also removes the notion of naive datetimes: each ``Pendulum`` instance is timezone-aware
-and by default in ``UTC`` for ease of use.
-
-Pendulum also improves the standard ``timedelta`` class by providing more intuitive methods and properties.
-
-
-Why not Arrow?
-==============
-
-Arrow is the most popular datetime library for Python right now, however its behavior
-and API can be erratic and unpredictable. The ``get()`` method can receive pretty much anything
-and it will try its best to return something while silently failing to handle some cases:
-
-.. code-block:: python
-
- arrow.get('2016-1-17')
- # <Arrow [2016-01-01T00:00:00+00:00]>
-
- pendulum.parse('2016-1-17')
- # <Pendulum [2016-01-17T00:00:00+00:00]>
-
- arrow.get('20160413')
- # <Arrow [1970-08-22T08:06:53+00:00]>
-
- pendulum.parse('20160413')
- # <Pendulum [2016-04-13T00:00:00+00:00]>
-
- arrow.get('2016-W07-5')
- # <Arrow [2016-01-01T00:00:00+00:00]>
-
- pendulum.parse('2016-W07-5')
- # <Pendulum [2016-02-19T00:00:00+00:00]>
-
- # Working with DST
- just_before = arrow.Arrow(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris')
- just_after = just_before.replace(microseconds=1)
- '2013-03-31T02:00:00+02:00'
- # Should be 2013-03-31T03:00:00+02:00
-
- (just_after.to('utc') - just_before.to('utc')).total_seconds()
- -3599.999999
- # Should be 1e-06
-
- just_before = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris')
- just_after = just_before.add(microseconds=1)
- '2013-03-31T03:00:00+02:00'
-
- (just_after.in_timezone('utc') - just_before.in_timezone('utc')).total_seconds()
- 1e-06
-
-Those are a few examples showing that Arrow cannot always be trusted to have a consistent
-behavior with the data you are passing to it.
-
-
-Limitations
-===========
-
-Even though the ``DateTime`` class is a subclass of ``datetime`` there are some rare cases where
-it can't replace the native class directly. Here is a list (non-exhaustive) of the reported cases with
-a possible solution, if any:
-
-* ``sqlite3`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter:
-
-.. code-block:: python
-
- from pendulum import DateTime
- from sqlite3 import register_adapter
-
- register_adapter(DateTime, lambda val: val.isoformat(' '))
-
-* ``mysqlclient`` (former ``MySQLdb``) and ``PyMySQL`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter:
-
-.. code-block:: python
-
- import MySQLdb.converters
- import pymysql.converters
-
- from pendulum import DateTime
-
- MySQLdb.converters.conversions[DateTime] = MySQLdb.converters.DateTime2literal
- pymysql.converters.conversions[DateTime] = pymysql.converters.escape_datetime
-
-* ``django`` will use the ``isoformat()`` method to store datetimes in the database. However since ``pendulum`` is always timezone aware the offset information will always be returned by ``isoformat()`` raising an error, at least for MySQL databases. To work around it you can either create your own ``DateTimeField`` or use the previous workaround for ``MySQLdb``:
-
-.. code-block:: python
-
- from django.db.models import DateTimeField as BaseDateTimeField
- from pendulum import DateTime
-
-
- class DateTimeField(BaseDateTimeField):
-
- def value_to_string(self, obj):
- val = self.value_from_object(obj)
-
- if isinstance(value, DateTime):
- return value.to_datetime_string()
-
- return '' if val is None else val.isoformat()
-
-
-Resources
-=========
-
-* `Official Website <https://pendulum.eustace.io>`_
-* `Documentation <https://pendulum.eustace.io/docs/>`_
-* `Issue Tracker <https://github.com/sdispater/pendulum/issues>`_
-
-
-Contributing
-============
-
-Contributions are welcome, especially with localization.
-
-Getting started
----------------
-
-To work on the Pendulum codebase, you'll want to clone the project locally
-and install the required depedendencies via `poetry <https://poetry.eustace.io>`_.
-
-.. code-block:: bash
-
- $ git clone git@github.com:sdispater/pendulum.git
- $ poetry install
-
-Localization
-------------
-
-If you want to help with localization, there are two different cases: the locale already exists
-or not.
-
-If the locale does not exist you will need to create it by using the ``clock`` utility:
-
-.. code-block:: bash
-
- ./clock locale create <your-locale>
-
-It will generate a directory in ``pendulum/locales`` named after your locale, with the following
-structure:
-
-.. code-block:: text
-
- <your-locale>/
- - custom.py
- - locale.py
-
-The ``locale.py`` file must not be modified. It contains the translations provided by
-the CLDR database.
-
-The ``custom.py`` file is the one you want to modify. It contains the data needed
-by Pendulum that are not provided by the CLDR database. You can take the `en <https://github.com/sdispater/pendulum/tree/master/pendulum/locales/en/custom.py>`_
-data as a reference to see which data is needed.
-
-You should also add tests for the created or modified locale.
-
diff --git a/README.rst b/README.rst
index d65fb47..78437e2 100644
--- a/README.rst
+++ b/README.rst
@@ -1,224 +1,176 @@
-Pendulum
-########
-
-.. image:: https://img.shields.io/pypi/v/pendulum.svg
- :target: https://pypi.python.org/pypi/pendulum
-
-.. image:: https://img.shields.io/pypi/l/pendulum.svg
- :target: https://pypi.python.org/pypi/pendulum
-
-.. image:: https://img.shields.io/codecov/c/github/sdispater/pendulum/master.svg
- :target: https://codecov.io/gh/sdispater/pendulum/branch/master
-
-.. image:: https://travis-ci.org/sdispater/pendulum.svg
- :alt: Pendulum Build status
- :target: https://travis-ci.org/sdispater/pendulum
-
-Python datetimes made easy.
-
-Supports Python **2.7** and **3.4+**.
-
-
-.. code-block:: python
-
- >>> import pendulum
-
- >>> now_in_paris = pendulum.now('Europe/Paris')
- >>> now_in_paris
- '2016-07-04T00:49:58.502116+02:00'
-
- # Seamless timezone switching
- >>> now_in_paris.in_timezone('UTC')
- '2016-07-03T22:49:58.502116+00:00'
-
- >>> tomorrow = pendulum.now().add(days=1)
- >>> last_week = pendulum.now().subtract(weeks=1)
-
- >>> past = pendulum.now().subtract(minutes=2)
- >>> past.diff_for_humans()
- >>> '2 minutes ago'
-
- >>> delta = past - last_week
- >>> delta.hours
- 23
- >>> delta.in_words(locale='en')
- '6 days 23 hours 58 minutes'
-
- # Proper handling of datetime normalization
- >>> pendulum.datetime(2013, 3, 31, 2, 30, tz='Europe/Paris')
- '2013-03-31T03:30:00+02:00' # 2:30 does not exist (Skipped time)
-
- # Proper handling of dst transitions
- >>> just_before = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, tz='Europe/Paris')
- '2013-03-31T01:59:59.999999+01:00'
- >>> just_before.add(microseconds=1)
- '2013-03-31T03:00:00+02:00'
-
-
-Why Pendulum?
-=============
-
-Native ``datetime`` instances are enough for basic cases but when you face more complex use-cases
-they often show limitations and are not so intuitive to work with.
-``Pendulum`` provides a cleaner and more easy to use API while still relying on the standard library.
-So it's still ``datetime`` but better.
-
-Unlike other datetime libraries for Python, Pendulum is a drop-in replacement
-for the standard ``datetime`` class (it inherits from it), so, basically, you can replace all your ``datetime``
-instances by ``DateTime`` instances in you code (exceptions exist for libraries that check
-the type of the objects by using the ``type`` function like ``sqlite3`` or ``PyMySQL`` for instance).
-
-It also removes the notion of naive datetimes: each ``Pendulum`` instance is timezone-aware
-and by default in ``UTC`` for ease of use.
-
-Pendulum also improves the standard ``timedelta`` class by providing more intuitive methods and properties.
-
-
-Why not Arrow?
-==============
-
-Arrow is the most popular datetime library for Python right now, however its behavior
-and API can be erratic and unpredictable. The ``get()`` method can receive pretty much anything
-and it will try its best to return something while silently failing to handle some cases:
-
-.. code-block:: python
-
- arrow.get('2016-1-17')
- # <Arrow [2016-01-01T00:00:00+00:00]>
-
- pendulum.parse('2016-1-17')
- # <Pendulum [2016-01-17T00:00:00+00:00]>
-
- arrow.get('20160413')
- # <Arrow [1970-08-22T08:06:53+00:00]>
-
- pendulum.parse('20160413')
- # <Pendulum [2016-04-13T00:00:00+00:00]>
-
- arrow.get('2016-W07-5')
- # <Arrow [2016-01-01T00:00:00+00:00]>
-
- pendulum.parse('2016-W07-5')
- # <Pendulum [2016-02-19T00:00:00+00:00]>
-
- # Working with DST
- just_before = arrow.Arrow(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris')
- just_after = just_before.replace(microseconds=1)
- '2013-03-31T02:00:00+02:00'
- # Should be 2013-03-31T03:00:00+02:00
-
- (just_after.to('utc') - just_before.to('utc')).total_seconds()
- -3599.999999
- # Should be 1e-06
-
- just_before = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris')
- just_after = just_before.add(microseconds=1)
- '2013-03-31T03:00:00+02:00'
-
- (just_after.in_timezone('utc') - just_before.in_timezone('utc')).total_seconds()
- 1e-06
-
-Those are a few examples showing that Arrow cannot always be trusted to have a consistent
-behavior with the data you are passing to it.
-
-
-Limitations
-===========
-
-Even though the ``DateTime`` class is a subclass of ``datetime`` there are some rare cases where
-it can't replace the native class directly. Here is a list (non-exhaustive) of the reported cases with
-a possible solution, if any:
-
-* ``sqlite3`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter:
-
-.. code-block:: python
-
- from pendulum import DateTime
- from sqlite3 import register_adapter
-
- register_adapter(DateTime, lambda val: val.isoformat(' '))
-
-* ``mysqlclient`` (former ``MySQLdb``) and ``PyMySQL`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter:
-
-.. code-block:: python
-
- import MySQLdb.converters
- import pymysql.converters
-
- from pendulum import DateTime
-
- MySQLdb.converters.conversions[DateTime] = MySQLdb.converters.DateTime2literal
- pymysql.converters.conversions[DateTime] = pymysql.converters.escape_datetime
-
-* ``django`` will use the ``isoformat()`` method to store datetimes in the database. However since ``pendulum`` is always timezone aware the offset information will always be returned by ``isoformat()`` raising an error, at least for MySQL databases. To work around it you can either create your own ``DateTimeField`` or use the previous workaround for ``MySQLdb``:
-
-.. code-block:: python
-
- from django.db.models import DateTimeField as BaseDateTimeField
- from pendulum import DateTime
-
-
- class DateTimeField(BaseDateTimeField):
-
- def value_to_string(self, obj):
- val = self.value_from_object(obj)
-
- if isinstance(value, DateTime):
- return value.to_datetime_string()
-
- return '' if val is None else val.isoformat()
-
-
-Resources
-=========
-
-* `Official Website <https://pendulum.eustace.io>`_
-* `Documentation <https://pendulum.eustace.io/docs/>`_
-* `Issue Tracker <https://github.com/sdispater/pendulum/issues>`_
-
-
-Contributing
-============
-
-Contributions are welcome, especially with localization.
-
-Getting started
----------------
-
-To work on the Pendulum codebase, you'll want to clone the project locally
-and install the required depedendencies via `poetry <https://poetry.eustace.io>`_.
-
-.. code-block:: bash
-
- $ git clone git@github.com:sdispater/pendulum.git
- $ poetry install
-
-Localization
-------------
-
-If you want to help with localization, there are two different cases: the locale already exists
-or not.
-
-If the locale does not exist you will need to create it by using the ``clock`` utility:
-
-.. code-block:: bash
-
- ./clock locale create <your-locale>
-
-It will generate a directory in ``pendulum/locales`` named after your locale, with the following
-structure:
-
-.. code-block:: text
-
- <your-locale>/
- - custom.py
- - locale.py
-
-The ``locale.py`` file must not be modified. It contains the translations provided by
-the CLDR database.
-
-The ``custom.py`` file is the one you want to modify. It contains the data needed
-by Pendulum that are not provided by the CLDR database. You can take the `en <https://github.com/sdispater/pendulum/tree/master/pendulum/locales/en/custom.py>`_
-data as a reference to see which data is needed.
-
-You should also add tests for the created or modified locale.
+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://github.com/sdispater/pendulum/actions/workflows/tests.yml/badge.svg
+ :alt: Pendulum Build status
+ :target: https://github.com/sdispater/pendulum/actions
+
+
+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.
+
+Limitations
+===========
+
+Even though the ``DateTime`` class is a subclass of ``datetime`` there are some rare cases where
+it can't replace the native class directly. Here is a list (non-exhaustive) of the reported cases with
+a possible solution, if any:
+
+* ``sqlite3`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter:
+
+.. code-block:: python
+
+ from pendulum import DateTime
+ from sqlite3 import register_adapter
+
+ register_adapter(DateTime, lambda val: val.isoformat(' '))
+
+* ``mysqlclient`` (former ``MySQLdb``) and ``PyMySQL`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter:
+
+.. code-block:: python
+
+ import MySQLdb.converters
+ import pymysql.converters
+
+ from pendulum import DateTime
+
+ MySQLdb.converters.conversions[DateTime] = MySQLdb.converters.DateTime2literal
+ pymysql.converters.conversions[DateTime] = pymysql.converters.escape_datetime
+
+* ``django`` will use the ``isoformat()`` method to store datetimes in the database. However since ``pendulum`` is always timezone aware the offset information will always be returned by ``isoformat()`` raising an error, at least for MySQL databases. To work around it you can either create your own ``DateTimeField`` or use the previous workaround for ``MySQLdb``:
+
+.. code-block:: python
+
+ from django.db.models import DateTimeField as BaseDateTimeField
+ from pendulum import DateTime
+
+
+ class DateTimeField(BaseDateTimeField):
+
+ def value_to_string(self, obj):
+ val = self.value_from_object(obj)
+
+ if isinstance(value, DateTime):
+ return value.to_datetime_string()
+
+ return '' if val is None else val.isoformat()
+
+
+Resources
+=========
+
+* `Official Website <https://pendulum.eustace.io>`_
+* `Documentation <https://pendulum.eustace.io/docs/>`_
+* `Issue Tracker <https://github.com/sdispater/pendulum/issues>`_
+
+
+Contributing
+============
+
+Contributions are welcome, especially with localization.
+
+Getting started
+---------------
+
+To work on the Pendulum codebase, you'll want to clone the project locally
+and install the required dependencies via `poetry <https://poetry.eustace.io>`_.
+
+.. code-block:: bash
+
+ $ git clone git@github.com:sdispater/pendulum.git
+ $ poetry install
+
+Localization
+------------
+
+If you want to help with localization, there are two different cases: the locale already exists
+or not.
+
+If the locale does not exist you will need to create it by using the ``clock`` utility:
+
+.. code-block:: bash
+
+ ./clock locale create <your-locale>
+
+It will generate a directory in ``pendulum/locales`` named after your locale, with the following
+structure:
+
+.. code-block:: text
+
+ <your-locale>/
+ - custom.py
+ - locale.py
+
+The ``locale.py`` file must not be modified. It contains the translations provided by
+the CLDR database.
+
+The ``custom.py`` file is the one you want to modify. It contains the data needed
+by Pendulum that are not provided by the CLDR database. You can take the `en <https://github.com/sdispater/pendulum/tree/master/pendulum/locales/en/custom.py>`_
+data as a reference to see which data is needed.
+
+You should also add tests for the created or modified locale.
diff --git a/build-wheels.sh b/build-wheels.sh
new file mode 100755
index 0000000..1c8f1cb
--- /dev/null
+++ b/build-wheels.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+set -e -x
+
+cd $(dirname $0)
+
+export PATH=/opt/python/cp38-cp38/bin/:$PATH
+
+curl -fsS -o get-poetry.py https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py
+/opt/python/cp38-cp38/bin/python get-poetry.py --preview -y
+rm get-poetry.py
+
+for PYBIN in /opt/python/cp3*/bin; do
+ if [ "$PYBIN" == "/opt/python/cp34-cp34m/bin" ]; then
+ continue
+ fi
+ if [ "$PYBIN" == "/opt/python/cp35-cp35m/bin" ]; then
+ continue
+ fi
+ rm -rf build
+ "${PYBIN}/python" $HOME/.poetry/bin/poetry build -vvv
+done
+
+cd dist
+for whl in *.whl; do
+ auditwheel repair "$whl"
+ rm "$whl"
+done
diff --git a/build.py b/build.py
index 07d277e..95e63e1 100644
--- a/build.py
+++ b/build.py
@@ -1,87 +1,33 @@
-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({})
+import subprocess
+
+from pathlib import Path
+
+
+def meson(*args):
+ subprocess.call(["meson"] + list(args))
+
+
+def _build():
+ build_dir = Path(__file__).parent.joinpath("build")
+ build_dir.mkdir(parents=True, exist_ok=True)
+
+ meson("setup", build_dir.as_posix())
+ meson("compile", "-C", build_dir.as_posix())
+ meson("install", "-C", build_dir.as_posix())
+
+
+def build(setup_kwargs):
+ """
+ This function is mandatory in order to build the extensions.
+ """
+ try:
+ _build()
+ except Exception:
+ print(
+ " Unable to build C extensions, "
+ "Pendulum will use the pure python version of the extensions."
+ )
+
+
+if __name__ == "__main__":
+ build({})
diff --git a/clock b/clock
new file mode 100755
index 0000000..17f35a3
--- /dev/null
+++ b/clock
@@ -0,0 +1,274 @@
+#!/usr/bin/env python
+
+from __future__ import annotations
+
+import glob
+import json
+import os
+
+from babel.core import get_global
+from babel.dates import PATTERN_CHARS
+from babel.dates import tokenize_pattern
+from babel.localedata import LocaleDataDict
+from babel.localedata import load
+from babel.localedata import normalize_locale
+from babel.plural import PluralRule
+from babel.plural import _binary_compiler
+from babel.plural import _GettextCompiler
+from babel.plural import _unary_compiler
+from babel.plural import compile_zero
+from cleo.application import Application
+from cleo.commands.command import Command
+from cleo.helpers import argument
+
+from pendulum import __version__
+
+
+class _LambdaCompiler(_GettextCompiler):
+ """Compiles the expression to lambda function."""
+
+ compile_v = compile_zero
+ compile_w = compile_zero
+ compile_f = compile_zero
+ compile_t = compile_zero
+ compile_and = _binary_compiler("(%s and %s)")
+ compile_or = _binary_compiler("(%s or %s)")
+ compile_not = _unary_compiler("(not %s)")
+ compile_mod = _binary_compiler("(%s %% %s)")
+
+ def compile_relation(self, method, expr, range_list):
+ code = _GettextCompiler.compile_relation(self, method, expr, range_list)
+ code = code.replace("&&", "and")
+ code = code.replace("||", "or")
+ if method == "in":
+ expr = self.compile(expr)
+ code = f"({expr} == {expr} and {code})"
+ return code
+
+
+class LocaleCreate(Command):
+
+ name = "locale create"
+ description = "Creates locale translations."
+
+ arguments = [argument("locales", "Locales to dump.", optional=False, multiple=True)]
+
+ TEMPLATE = """from .custom import translations as custom_translations
+
+
+\"\"\"
+{locale} locale file.
+
+It has been generated automatically and must not be modified directly.
+\"\"\"
+
+
+locale = {{
+ 'plural': {plural},
+ 'ordinal': {ordinal},
+ 'translations': {translations},
+ 'custom': custom_translations
+}}
+"""
+
+ CUSTOM_TEMPLATE = """\"\"\"
+{locale} custom locale file.
+\"\"\"
+
+translations = {{}}
+"""
+
+ LOCALE_DIR = os.path.join("pendulum", "locales")
+
+ def handle(self):
+ locales = self.argument("locales")
+ if not locales:
+ return
+
+ for locale in locales:
+ data = {}
+ parts = locale.split("-")
+ if len(parts) > 1:
+ parts[1] = parts[1].upper()
+
+ normalized = normalize_locale(locale.replace("-", "_"))
+ if not normalized:
+ self.line(f"<error>Locale [{locale}] does not exist.</error>")
+ continue
+
+ self.line(f"<info>Generating <comment>{locale}</> locale.</>")
+
+ content = LocaleDataDict(load(normalized))
+
+ # Pluralization rule
+ rule = content["plural_form"]
+ plural = self.plural_rule_to_lambda(rule)
+
+ # Ordinal rule
+ rule = content["ordinal_form"]
+ ordinal = self.plural_rule_to_lambda(rule)
+
+ # Getting days names
+ days = content["days"]["format"]
+ data["days"] = {}
+ for fmt, names in days.items():
+ data["days"][fmt] = {}
+ for value, name in names.items():
+ data["days"][fmt][(value + 1) % 7] = name
+
+ # Getting months names
+ months = content["months"]["format"]
+ data["months"] = months
+
+ # Units
+ patterns = content["unit_patterns"]
+ units = [
+ "year",
+ "month",
+ "week",
+ "day",
+ "hour",
+ "minute",
+ "second",
+ "microsecond",
+ ]
+ data["units"] = {}
+ for unit in units:
+ pattern = patterns[f"duration-{unit}"]["long"]
+ if "per" in pattern:
+ del pattern["per"]
+
+ data["units"][unit] = pattern
+
+ # Relative
+ data["relative"] = {}
+ for key in content["date_fields"]:
+ if key not in [
+ "year",
+ "month",
+ "week",
+ "day",
+ "hour",
+ "minute",
+ "second",
+ ]:
+ continue
+
+ data["relative"][key] = content["date_fields"][key]
+
+ # Day periods
+ data["day_periods"] = content["day_periods"]["format"]["wide"]
+
+ result = self.TEMPLATE.format(
+ locale=locale,
+ plural=plural,
+ ordinal=ordinal,
+ translations=self.format_dict(data, tab=2),
+ )
+
+ dest_dir = os.path.join(self.LOCALE_DIR, locale.replace("-", "_"))
+ if not os.path.exists(dest_dir):
+ os.mkdir(dest_dir)
+
+ init = os.path.join(dest_dir, "__init__.py")
+ main = os.path.join(dest_dir, "locale.py")
+ custom = os.path.join(dest_dir, "custom.py")
+
+ if not os.path.exists(init):
+ with open(init, "w"):
+ os.utime(init)
+
+ with open(main, "w") as fw:
+ fw.write(result)
+
+ if not os.path.exists(custom):
+ with open(custom, "w") as fw:
+ fw.write(self.CUSTOM_TEMPLATE.format(locale=locale))
+
+ def format_dict(self, d, tab=1):
+ s = ["{\n"]
+ for k, v in d.items():
+ if isinstance(v, (dict, LocaleDataDict)):
+ v = self.format_dict(v, tab + 1)
+ else:
+ v = repr(v)
+
+ s.append(f"{' ' * tab}{k!r}: {v},\n")
+ s.append(f'{" " * (tab - 1)}}}')
+
+ return "".join(s)
+
+ def plural_rule_to_lambda(self, rule):
+ to_py = _LambdaCompiler().compile
+ result = ["lambda n: "]
+ for tag, ast in PluralRule.parse(rule).abstract:
+ result.append(f"'{tag}' if {to_py(ast)} else ")
+ result.append("'other'")
+ return "".join(result)
+
+ def convert_ldml_format(self, fmt):
+ result = []
+
+ for tok_type, tok_value in tokenize_pattern(fmt):
+ if tok_type == "chars":
+ result.append(tok_value.replace("%", "%%"))
+ elif tok_type == "field":
+ fieldchar, fieldnum = tok_value
+ limit = PATTERN_CHARS[fieldchar]
+ if limit and fieldnum not in limit:
+ raise ValueError(
+ f"Invalid length for field: {(fieldchar * fieldnum)!r}"
+ )
+ result.append(
+ self.TOKENS_MAP.get(fieldchar * fieldnum, fieldchar * fieldnum)
+ )
+ else:
+ raise NotImplementedError(f"Unknown token type: {tok_type}")
+
+ return "".join(result)
+
+
+class LocaleRecreate(Command):
+
+ name = "locale recreate"
+ description = "Recreate existing locales."
+
+ def handle(self):
+ # Listing locales
+
+ locales_dir = os.path.join("pendulum", "locales")
+ locales = glob.glob(os.path.join(locales_dir, "*", "locale.py"))
+ locales = [os.path.basename(os.path.dirname(locale)) for locale in locales]
+
+ self.call("locale:create", [("locales", locales)])
+
+
+class WindowsTzDump(Command):
+
+ name = "windows dump-timezones"
+ description = "Dumps the mapping of Windows timezones to IANA timezones."
+
+ MAPPING_DIR = os.path.join("pendulum", "tz", "data")
+
+ def handle(self):
+ raw_tznames = get_global("windows_zone_mapping")
+ sorted_names = sorted(raw_tznames.keys())
+
+ tznames = {}
+ for name in sorted_names:
+ tznames[name] = raw_tznames[name]
+
+ mapping = json.dumps(tznames, indent=4).replace('"', "'")
+
+ with open(os.path.join(self.MAPPING_DIR, "windows.py"), "w") as f:
+ f.write(f"windows_timezones = {mapping}\n")
+
+
+app = Application("clock", __version__)
+app.add(LocaleCreate())
+app.add(LocaleRecreate())
+app.add(WindowsTzDump())
+
+
+if __name__ == "__main__":
+ app.run()
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..3cbba28
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,7 @@
+comment: false
+
+coverage:
+ status:
+ patch:
+ default:
+ enabled: false
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..9b3dd7c
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,2 @@
+html:
+ mkdocs build
diff --git a/docs/docs/addition_subtraction.md b/docs/docs/addition_subtraction.md
new file mode 100644
index 0000000..686f67f
--- /dev/null
+++ b/docs/docs/addition_subtraction.md
@@ -0,0 +1,87 @@
+# Addition and Subtraction
+
+To easily add and subtract time, you can use the `add()` and `subtract()`
+methods.
+Each method returns a new `DateTime` instance.
+
+```python
+>>> import pendulum
+
+>>> dt = pendulum.datetime(2012, 1, 31)
+
+>>> dt.to_datetime_string()
+'2012-01-31 00:00:00'
+
+>>> dt = dt.add(years=5)
+'2017-01-31 00:00:00'
+>>> dt = dt.add(years=1)
+'2018-01-31 00:00:00'
+>>> dt = dt.subtract(years=1)
+'2017-01-31 00:00:00'
+>>> dt = dt.subtract(years=5)
+'2012-01-31 00:00:00'
+
+>>> dt = dt.add(months=60)
+'2017-01-31 00:00:00'
+>>> dt = dt.add(months=1)
+'2017-02-28 00:00:00'
+>>> dt = dt.subtract(months=1)
+'2017-01-28 00:00:00'
+>>> dt = dt.subtract(months=60)
+'2012-01-28 00:00:00'
+
+>>> dt = dt.add(days=29)
+'2012-02-26 00:00:00'
+>>> dt = dt.add(days=1)
+'2012-02-27 00:00:00'
+>>> dt = dt.subtract(days=1)
+'2012-02-26 00:00:00'
+>>> dt = dt.subtract(days=29)
+'2012-01-28 00:00:00'
+
+>>> dt = dt.add(weeks=3)
+'2012-02-18 00:00:00'
+>>> dt = dt.add(weeks=1)
+'2012-02-25 00:00:00'
+>>> dt = dt.subtract(weeks=1)
+'2012-02-18 00:00:00'
+>>> dt = dt.subtract(weeks=3)
+'2012-01-28 00:00:00'
+
+>>> dt = dt.add(hours=24)
+'2012-01-29 00:00:00'
+>>> dt = dt.add(hours=1)
+'2012-02-25 01:00:00'
+>>> dt = dt.subtract(hours=1)
+'2012-02-29 00:00:00'
+>>> dt = dt.subtract(hours=24)
+'2012-01-28 00:00:00'
+
+>>> dt = dt.add(minutes=61)
+'2012-01-28 01:01:00'
+>>> dt = dt.add(minutes=1)
+'2012-01-28 01:02:00'
+>>> dt = dt.subtract(minutes=1)
+'2012-01-28 01:01:00'
+>>> dt = dt.subtract(minutes=24)
+'2012-01-28 00:00:00'
+
+>>> dt = dt.add(seconds=61)
+'2012-01-28 00:01:01'
+>>> dt = dt.add(seconds=1)
+'2012-01-28 00:01:02'
+>>> dt = dt.subtract(seconds=1)
+'2012-01-28 00:01:01'
+>>> dt = dt.subtract(seconds=61)
+'2012-01-28 00:00:00'
+
+>>> dt = dt.add(years=3, months=2, days=6, hours=12, minutes=31, seconds=43)
+'2015-04-03 12:31:43'
+>>> dt = dt.subtract(years=3, months=2, days=6, hours=12, minutes=31, seconds=43)
+'2012-01-28 00:00:00'
+```
+
+!!!note
+
+ Passing negative values to `add()` is also possible and will act exactly
+ like `subtract()`
diff --git a/docs/docs/attributes_properties.md b/docs/docs/attributes_properties.md
new file mode 100644
index 0000000..290891e
--- /dev/null
+++ b/docs/docs/attributes_properties.md
@@ -0,0 +1,87 @@
+# Attributes and Properties
+
+Pendulum gives access to more attributes and properties than the default ``datetime`` class.
+
+```python
+>>> import pendulum
+
+>>> dt = pendulum.parse('2012-09-05T23:26:11.123789')
+
+# These properties specifically return integers
+>>> dt.year
+2012
+>>> dt.month
+9
+>>> dt.day
+5
+>>> dt.hour
+23
+>>> dt.minute
+26
+>>> dt.second
+11
+>>> dt.microsecond
+123789
+>>> dt.day_of_week
+3
+>>> dt.day_of_year
+248
+>>> dt.week_of_month
+1
+>>> dt.week_of_year
+36
+>>> dt.days_in_month
+30
+>>> dt.timestamp()
+1346887571.123789
+>>> dt.float_timestamp
+1346887571.123789
+>>> dt.int_timestamp
+1346887571
+
+>>> pendulum.datetime(1975, 5, 21).age
+41 # calculated vs now in the same tz
+>>> dt.quarter
+3
+
+# Returns an int of seconds difference from UTC (+/- sign included)
+>>> pendulum.from_timestamp(0).offset
+0
+>>> pendulum.from_timestamp(0, 'America/Toronto').offset
+-18000
+
+# Returns a float of hours difference from UTC (+/- sign included)
+>>> pendulum.from_timestamp(0, 'America/Toronto').offset_hours
+-5.0
+>>> pendulum.from_timestamp(0, 'Australia/Adelaide').offset_hours
+9.5
+
+# Gets the timezone instance
+>>> pendulum.now().timezone
+>>> pendulum.now().tz
+
+# Gets the timezone name
+>>> pendulum.now().timezone_name
+
+# Indicates if daylight savings time is on
+>>> dt = pendulum.datetime(2012, 1, 1, tz='America/Toronto')
+>>> dt.is_dst()
+False
+>>> dt = pendulum.datetime(2012, 9, 1, tz='America/Toronto')
+>>> dt.is_dst()
+True
+
+# Indicates if the instance is in the same timezone as the local timezone
+>>> pendulum.now().is_local()
+True
+>>> pendulum.now('Europe/London').is_local()
+False
+
+# Indicates if the instance is in the UTC timezone
+>>> pendulum.now().is_utc()
+False
+>>> pendulum.now('Europe/London').is_local()
+False
+>>> pendulum.now('UTC').is_utc()
+True
+```
diff --git a/docs/docs/comparison.md b/docs/docs/comparison.md
new file mode 100644
index 0000000..6be8d0d
--- /dev/null
+++ b/docs/docs/comparison.md
@@ -0,0 +1,77 @@
+# Comparison
+
+Simple comparison is offered up via the basic operators.
+Remember that the comparison is done in the UTC timezone
+so things aren't always as they seem.
+
+```python
+>>> import pendulum
+
+>>> first = pendulum.datetime(2012, 9, 5, 23, 26, 11, 0, tz='America/Toronto')
+>>> second = pendulum.datetime(2012, 9, 5, 20, 26, 11, 0, tz='America/Vancouver')
+
+>>> first.to_datetime_string()
+'2012-09-05 23:26:11'
+>>> first.timezone_name
+'America/Toronto'
+>>> second.to_datetime_string()
+'2012-09-05 20:26:11'
+>>> second.timezone_name
+'America/Vancouver'
+
+>>> first == second
+True
+>>> first != second
+False
+>>> first > second
+False
+>>> first >= second
+True
+>>> first < second
+False
+>>> first <= second
+True
+
+>>> first = first.on(2012, 1, 1).at(0, 0, 0)
+>>> second = second.on(2012, 1, 1).at(0, 0, 0)
+# tz is still America/Vancouver for second
+
+>>> first == second
+False
+>>> first != second
+True
+>>> first > second
+False
+>>> first >= second
+False
+>>> first < second
+True
+>>> first <= second
+True
+```
+
+To handle the most used cases there are some simple helper functions.
+For the methods that compare to `now()` (ex. `is_today()`) in some manner
+the `now()` is created in the same timezone as the instance.
+
+```python
+>>> import pendulum
+
+>>> dt = pendulum.now()
+
+>>> dt.is_past()
+>>> dt.is_leap_year()
+
+>>> born = pendulum.datetime(1987, 4, 23)
+>>> not_birthday = pendulum.datetime(2014, 9, 26)
+>>> birthday = pendulum.datetime(2014, 4, 23)
+>>> past_birthday = pendulum.now().subtract(years=50)
+
+>>> born.is_birthday(not_birthday)
+False
+>>> born.is_birthday(birthday)
+True
+>>> past_birthday.is_birthday()
+# Compares to now by default
+True
+```
diff --git a/docs/docs/difference.md b/docs/docs/difference.md
new file mode 100644
index 0000000..3a7f063
--- /dev/null
+++ b/docs/docs/difference.md
@@ -0,0 +1,115 @@
+# Difference
+
+The `diff()` method returns a [Period](#period) instance that represents the total duration
+between two `DateTime` instances. This interval can be then expressed in various units.
+These interval methods always return *the total difference expressed* in the specified time requested.
+All values are truncated and not rounded.
+
+The `diff()` method has a default first parameter which is the `DateTime` instance to compare to,
+or `None` if you want to use `now()`.
+The 2nd parameter is optional and indicates if you want the return value to be the absolute value
+or a relative value that might have a `-` (negative) sign if the passed in date
+is less than the current instance.
+This will default to `True`, return the absolute value.
+
+```python
+>>> import pendulum
+
+>>> dt_ottawa = pendulum.datetime(2000, 1, 1, tz='America/Toronto')
+>>> dt_vancouver = pendulum.datetime(2000, 1, 1, tz='America/Vancouver')
+
+>>> dt_ottawa.diff(dt_vancouver).in_hours()
+3
+>>> dt_ottawa.diff(dt_vancouver, False).in_hours()
+3
+>>> dt_vancouver.diff(dt_ottawa, False).in_hours()
+-3
+
+>>> dt = pendulum.datetime(2012, 1, 31, 0)
+>>> dt.diff(dt.add(months=1)).in_days()
+29
+>>> dt.diff(dt.subtract(months=1), False).in_days()
+-31
+
+>>> dt = pendulum.datetime(2012, 4, 30, 0)
+>>> dt.diff(dt.add(months=1)).in_days()
+30
+>>> dt.diff(dt.add(weeks=1)).in_days()
+7
+
+>>> dt = pendulum.datetime(2012, 1, 1, 0)
+>>> dt.diff(dt.add(seconds=59)).in_minutes()
+0
+>>> dt.diff(dt.add(seconds=60)).in_minutes()
+1
+>>> dt.diff(dt.add(seconds=119)).in_minutes()
+1
+>>> dt.diff(dt.add(seconds=120)).in_minutes()
+2
+```
+
+Difference for Humans
+---------------------
+
+The `diff_for_humans()` method will add a phrase after the difference value relative
+to the instance and the passed in instance. There are 4 possibilities:
+
+* When comparing a value in the past to default now:
+ * 1 hour ago
+ * 5 months ago
+
+* When comparing a value in the future to default now:
+ * 1 hour from now
+ * 5 months from now
+
+* When comparing a value in the past to another value:
+ * 1 hour before
+ * 5 months before
+
+* When comparing a value in the future to another value:
+ * 1 hour after
+ * 5 months after
+
+You may also pass `True` as a 2nd parameter to remove the modifiers `ago`, `from now`, etc.
+
+```python
+>>> import pendulum
+
+# The most typical usage is for comments
+# The instance is the date the comment was created
+# and its being compared to default now()
+>>> pendulum.now().subtract(days=1).diff_for_humans()
+'1 day ago'
+
+>>> pendulum.now().diff_for_humans(pendulum.now().subtract(years=1))
+'1 year after'
+
+>>> dt = pendulum.datetime(2011, 8, 1)
+>>> dt.diff_for_humans(dt.add(months=1))
+'1 month before'
+>>> dt.diff_for_humans(dt.subtract(months=1))
+'1 month after'
+
+>>> pendulum.now().add(seconds=5).diff_for_humans()
+'5 seconds from now'
+
+>>> pendulum.now().subtract(days=24).diff_for_humans()
+'3 weeks ago'
+
+>>> pendulum.now().subtract(days=24).diff_for_humans(absolute=True)
+'3 weeks'
+```
+
+You can also change the locale of the string either globally by using `pendulum.set_locale('fr')`
+before the `diff_for_humans()` call or specifically for the call by passing the `locale` keyword
+argument. See the [Localization](#localization) section for more detail.
+
+```python
+>>> import pendulum
+
+>>> pendulum.set_locale('de')
+>>> pendulum.now().add(years=1).diff_for_humans()
+'in 1 Jahr'
+>>> pendulum.now().add(years=1).diff_for_humans(locale='fr')
+'dans 1 an'
+```
diff --git a/docs/docs/duration.md b/docs/docs/duration.md
new file mode 100644
index 0000000..0801d9e
--- /dev/null
+++ b/docs/docs/duration.md
@@ -0,0 +1,177 @@
+# Duration
+
+The `Duration` class is inherited from the native `timedelta` class.
+It has many improvements over the base class.
+
+!!!note
+
+ Even though, it inherits from the `timedelta` class, its behavior is slightly different.
+ The more important to notice is that the native normalization does not happen, this is so that
+ it feels more intuitive.
+
+ ```python
+ >>> import pendulum
+ >>> from datetime import datetime
+
+ >>> d1 = datetime(2012, 1, 1, 1, 2, 3, tzinfo=pytz.UTC)
+ >>> d2 = datetime(2011, 12, 31, 22, 2, 3, tzinfo=pytz.UTC)
+ >>> delta = d2 - d1
+ >>> delta.days
+ -1
+ >>> delta.seconds
+ 75600
+
+ >>> d1 = pendulum.datetime(2012, 1, 1, 1, 2, 3)
+ >>> d2 = pendulum.datetime(2011, 12, 31, 22, 2, 3)
+ >>> delta = d2 - d1
+ >>> delta.days
+ 0
+ >>> delta.hours
+ -3
+ ```
+
+## Instantiation
+
+To create a `Duration` instance, you can use the `duration()` helper:
+
+```python
+>>> import pendulum
+
+>>> it = pendulum.duration(days=1177, seconds=7284, microseconds=1234)
+```
+
+!!!note
+
+ Unlike the native `timedelta` class, durations support specifying
+ years and months.
+
+ ```python
+ >>> import pendulum
+
+ >>> it = pendulum.duration(years=2, months=3)
+ ```
+
+ However, to maintain compatibility, native methods and properties will
+ make approximations:
+
+ ```python
+ >>> it.days
+ 820
+
+ >>> it.total_seconds()
+ 70848000.0
+ ```
+
+## Properties and Duration Methods
+
+The `Duration` class brings more properties than the default `days`, `seconds` and
+`microseconds`.
+
+```python
+>>> import pendulum
+
+>>> it = pendulum.duration(
+... years=2, months=3,
+... days=1177, seconds=7284, microseconds=1234
+... )
+
+>>> it.years
+2
+>>> it.months
+3
+
+# Weeks are based on the total of days
+# It does not take into account years and months
+>>> it.weeks
+168
+
+# Days, just like in timedelta, represents the total of days
+# in the duration. If years and/or months are specified
+# it will use an approximation
+>>> it.days
+1997
+
+# If you want the remaining days not included in full weeks
+>>> it.remaining_days
+1
+
+>>> # The remaining number in each unit
+>>> it.hours
+2
+>>> it.minutes
+1
+
+# Seconds are, like days, a special case and the default
+# property will return the whole value of remaining
+# seconds just like the timedelta class for compatibility
+>>> it.seconds
+7284
+
+# If you want the number of seconds not included
+# in hours and minutes
+>>> it.remaining_seconds
+24
+
+>>> it.microseconds
+1234
+```
+
+If you want to get the duration in each supported unit
+you can use the appropriate methods.
+
+```python
+# Each method returns a float like the native
+# total_seconds() method
+>>> it.total_weeks()
+168.15490079569113
+
+>>> it.total_days()
+1177.0843055698379
+
+>>> it.total_hours()
+28250.02333367611
+
+>>> it.total_minutes()
+1695001.4000205665
+
+>>> it.total_seconds()
+101700084.001234
+```
+
+Similarly, the `in_xxx()` methods return the total duration in each
+supported unit as a truncated integer.
+
+```python
+>>> it.in_weeks()
+168
+
+>>> it.in_days()
+1997
+
+>>> it.in_hours()
+28250
+
+>>> it.in_minutes()
+1695001
+
+>>> it.in_seconds()
+101700084
+```
+
+It also has a handy `in_words()` method, which determines the duration representation when printed.
+
+```python
+>>> import pendulum
+
+>>> pendulum.set_locale('fr')
+
+>>> it = pendulum.duration(days=1177, seconds=7284, microseconds=1234)
+>>> it.in_words()
+'168 semaines 1 jour 2 heures 1 minute 24 secondes'
+
+>>> print(it)
+'168 semaines 1 jour 2 heures 1 minute 24 secondes'
+
+>>> it.in_words(locale='de')
+'168 Wochen 1 Tag 2 Stunden 1 Minute 24 Sekunden'
+```
diff --git a/docs/docs/fluent_helpers.md b/docs/docs/fluent_helpers.md
new file mode 100644
index 0000000..2ace491
--- /dev/null
+++ b/docs/docs/fluent_helpers.md
@@ -0,0 +1,67 @@
+# Fluent helpers
+
+Pendulum provides helpers that return a new instance with some attributes
+modified compared to the original instance.
+However, none of these helpers, with the exception of explicitly setting the
+timezone, will change the timezone of the instance. Specifically,
+setting the timestamp will not set the corresponding timezone to UTC.
+
+```python
+>>> import pendulum
+
+>>> dt = pendulum.now()
+
+>>> dt.set(year=1975, month=5, day=21).to_datetime_string()
+'1975-05-21 13:45:18'
+
+>>> dt.set(hour=22, minute=32, second=5).to_datetime_string()
+'2016-11-16 22:32:05'
+```
+
+You can also use the `on()` and `at()` methods to change the date and the time
+respectively
+
+```python
+>>> dt.on(1975, 5, 21).at(22, 32, 5).to_datetime_string()
+'1975-05-21 22:32:05'
+
+>>> dt.at(10).to_datetime_string()
+'2016-11-16 10:00:00'
+
+>>> dt.at(10, 30).to_datetime_string()
+'2016-11-16 10:30:00'
+```
+
+You can also modify the timezone.
+
+```python
+>>> dt.set(tz='Europe/London')
+```
+
+Setting the timezone just modifies the timezone information without
+making any conversion, while `in_timezone()` (or `in_tz()`)
+converts the time in the appropriate timezone.
+
+```python
+>>> import pendulum
+
+>>> dt = pendulum.datetime(2013, 3, 31, 2, 30)
+>>> print(dt)
+'2013-03-31T02:30:00+00:00'
+
+>>> dt = dt.set(tz='Europe/Paris')
+>>> print(dt)
+'2013-03-31T03:30:00+02:00'
+
+>>> dt = dt.in_tz('Europe/Paris')
+>>> print(dt)
+'2013-03-31T04:30:00+02:00'
+
+>>> dt = dt.set(tz='Europe/Paris').set(tz='UTC')
+>>> print(dt)
+'2013-03-31T03:30:00+00:00'
+
+>>> dt = dt.in_tz('Europe/Paris').in_tz('UTC')
+>>> print(dt)
+'2013-03-31T02:30:00+00:00'
+```
diff --git a/docs/docs/index.md b/docs/docs/index.md
new file mode 100644
index 0000000..daca205
--- /dev/null
+++ b/docs/docs/index.md
@@ -0,0 +1,17 @@
+{!docs/installation.md!}
+{!docs/introduction.md!}
+{!docs/instantiation.md!}
+{!docs/parsing.md!}
+{!docs/localization.md!}
+{!docs/attributes_properties.md!}
+{!docs/fluent_helpers.md!}
+{!docs/string_formatting.md!}
+{!docs/comparison.md!}
+{!docs/addition_subtraction.md!}
+{!docs/difference.md!}
+{!docs/modifiers.md!}
+{!docs/timezones.md!}
+{!docs/duration.md!}
+{!docs/period.md!}
+{!docs/testing.md!}
+{!docs/limitations.md!}
diff --git a/docs/docs/installation.md b/docs/docs/installation.md
new file mode 100644
index 0000000..bfa9641
--- /dev/null
+++ b/docs/docs/installation.md
@@ -0,0 +1,13 @@
+# Installation
+
+Installing `pendulum` is quite simple:
+
+```bash
+$ pip install pendulum
+```
+
+or, if you are using [poetry](https://python-poetry.org):
+
+```bash
+$ poetry add pendulum
+```
diff --git a/docs/docs/instantiation.md b/docs/docs/instantiation.md
new file mode 100644
index 0000000..a49c6a2
--- /dev/null
+++ b/docs/docs/instantiation.md
@@ -0,0 +1,144 @@
+# Instantiation
+
+There are several different methods available to create a new `DateTime` instance.
+
+First there is the main `datetime()` helper.
+
+```python
+>>> import pendulum
+
+>>> dt = pendulum.datetime(2015, 2, 5)
+>>> isinstance(dt, datetime)
+True
+>>> dt.timezone.name
+'UTC'
+```
+
+`datetime()` sets the time to `00:00:00` if it's not specified,
+and the timezone (the `tz` keyword argument) to `UTC`.
+Otherwise it can be a `Timezone` instance or simply a string timezone value.
+
+```python
+>>> import pendulum
+
+>>> pendulum.datetime(2015, 2, 5, tz='Europe/Paris')
+>>> tz = pendulum.timezone('Europe/Paris')
+>>> pendulum.datetime(2015, 2, 5, tz=tz)
+```
+
+!!!note
+
+ Supported strings for timezones are the one provided
+ by the [IANA time zone database](https://www.iana.org/time-zones).
+
+ The special `local` string is also supported and will return your current timezone.
+
+!!!warning
+
+ The `tz` argument is keyword-only, unlike in version `1.x`
+
+The `local()` helper is similar to `datetime()` but automatically sets the
+timezone to the local timezone.
+
+```python
+>>> import pendulum
+
+>>> dt = pendulum.local(2015, 2, 5)
+>>> print(dt.timezone.name)
+'America/Toronto'
+```
+
+!!!note
+
+ `local()` is just an alias for `datetime(..., tz='local')`.
+
+There is also the `now()` method.
+
+```python
+>>> import pendulum
+
+>>> now = pendulum.now()
+
+>>> now_in_london_tz = pendulum.now('Europe/London')
+>>> now_in_london_tz.timezone_name
+'Europe/London'
+```
+
+To accompany `now()`, a few other static instantiation helpers exist to create known instances.
+The only thing to really notice here is that `today()`, `tomorrow()` and `yesterday()`,
+besides behaving as expected, all accept a timezone parameter
+and each has their time value set to `00:00:00`.
+
+```python
+>>> now = pendulum.now()
+>>> print(now)
+'2016-06-28T16:51:45.978473-05:00'
+
+>>> today = pendulum.today()
+>>> print(today)
+'2016-06-28T00:00:00-05:00'
+
+>>> tomorrow = pendulum.tomorrow('Europe/London')
+>>> print(tomorrow)
+'2016-06-29T00:00:00+01:00'
+
+>>> yesterday = pendulum.yesterday()
+>>> print(yesterday)
+'2016-06-27T00:00:00-05:00'
+```
+
+Pendulum enforces timezone aware datetimes, and using them is the preferred and recommended way
+of using the library. However, if you really need a **naive** `DateTime` object, the `naive()` helper
+is there for you.
+
+```python
+>>> import pendulum
+
+>>> naive = pendulum.naive(2015, 2, 5)
+>>> naive.timezone
+None
+```
+
+The next helper, `from_format()`, is similar to the native `datetime.strptime()` function
+but uses custom tokens to create a `DateTime` instance.
+
+```python
+>>> dt = pendulum.from_format('1975-05-21 22', 'YYYY-MM-DD HH')
+>>> print(dt)
+'1975-05-21T22:00:00+00:00'
+```
+
+!!!note
+
+ To see all the available tokens, you can check the [Formatter](#formatter) section.
+
+It also accepts a `tz` keyword argument to specify the timezone:
+
+```python
+>>> dt = pendulum.from_format('1975-05-21 22', 'YYYY-MM-DD HH', tz='Europe/London')
+'1975-05-21T22:00:00+01:00'
+```
+
+The final helper is for working with unix timestamps.
+`from_timestamp()` will create a `DateTime` instance equal to the given timestamp
+and will set the timezone as well or default it to `UTC`.
+
+```python
+>>> dt = pendulum.from_timestamp(-1)
+>>> print(dt)
+'1969-12-31T23:59:59+00:00'
+
+>>> dt = pendulum.from_timestamp(-1, tz='Europe/London')
+>>> print(dt)
+'1970-01-01T00:59:59+01:00'
+```
+
+Finally, if you find yourself inheriting a `datetime.datetime` instance,
+you can create a `DateTime` instance via the `instance()` function.
+
+```python
+>>> dt = datetime(2008, 1, 1)
+>>> p = pendulum.instance(dt)
+>>> print(p)
+'2008-01-01T00:00:00+00:00'
+```
diff --git a/docs/docs/introduction.md b/docs/docs/introduction.md
new file mode 100644
index 0000000..fbbab97
--- /dev/null
+++ b/docs/docs/introduction.md
@@ -0,0 +1,21 @@
+# Introduction
+
+Pendulum is a Python package to ease datetimes manipulation.
+
+It provides classes that are drop-in replacements for the native ones (they inherit from them).
+
+Special care has been taken to ensure timezones are handled correctly,
+and are based on the underlying `tzinfo` implementation.
+For example, all comparisons are done in `UTC` or in the timezone of the datetime being used.
+
+```python
+>>> import pendulum
+
+>>> dt_toronto = pendulum.datetime(2012, 1, 1, tz='America/Toronto')
+>>> dt_vancouver = pendulum.datetime(2012, 1, 1, tz='America/Vancouver')
+
+>>> print(dt_vancouver.diff(dt_toronto).in_hours())
+3
+```
+
+The default timezone, except when using the `now()`, method will always be `UTC`.
diff --git a/docs/docs/limitations.md b/docs/docs/limitations.md
new file mode 100644
index 0000000..7deff23
--- /dev/null
+++ b/docs/docs/limitations.md
@@ -0,0 +1,43 @@
+# 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 the `type()` function to determine the type of the object by default. To work around it you can register a new adapter:
+
+ ```python
+ import pendulum
+ from sqlite3 import register_adapter
+
+ register_adapter(pendulum.DateTime, lambda val: val.isoformat(' '))
+ ```
+
+* `mysqlclient` (former `MySQLdb`) and `PyMySQL` will use the the `type()` function to determine the type of the object by default. To work around it you can register a new adapter:
+
+ ```python
+ import pendulum
+ import MySQLdb.converters
+ import pymysql.converters
+
+ MySQLdb.converters.conversions[pendulum.DateTime] = MySQLdb.converters.DateTime2literal
+ pymysql.converters.conversions[pendulum.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`:
+
+ ```python
+ import pendulum
+ from django.db.models import DateTimeField as BaseDateTimeField
+
+
+ class DateTimeField(BaseDateTimeField):
+
+ def value_to_string(self, obj):
+ val = self.value_from_object(obj)
+
+ if isinstance(value, pendulum.DateTime):
+ return value.format('YYYY-MM-DD HH:mm:ss')
+
+ return '' if val is None else val.isoformat()
+ ```
diff --git a/docs/docs/localization.md b/docs/docs/localization.md
new file mode 100644
index 0000000..7560dae
--- /dev/null
+++ b/docs/docs/localization.md
@@ -0,0 +1,36 @@
+# Localization
+
+Localization occurs when using the `format()` method which accepts a `locale` keyword.
+
+```python
+>>> import pendulum
+
+>>> dt = pendulum.datetime(1975, 5, 21)
+>>> dt.format('dddd DD MMMM YYYY', locale='de')
+'Mittwoch 21 Mai 1975'
+
+>>> dt.format('dddd DD MMMM YYYY')
+'Wednesday 21 May 1975'
+```
+
+`diff_for_humans()` is also localized, you can set the locale
+by using `pendulum.set_locale()`.
+
+```python
+>>> import pendulum
+
+>>> pendulum.set_locale('de')
+>>> pendulum.now().add(years=1).diff_for_humans()
+'in 1 Jahr'
+>>> pendulum.set_locale('en')
+```
+
+However, you might not want to set the locale globally. The `diff_for_humans()`
+method accepts a `locale` keyword argument to use a locale for a specific call.
+
+```python
+>>> pendulum.set_locale('de')
+>>> dt = pendulum.now().add(years=1)
+>>> dt.diff_for_humans(locale='fr')
+'dans 1 an'
+```
diff --git a/docs/docs/modifiers.md b/docs/docs/modifiers.md
new file mode 100644
index 0000000..b440587
--- /dev/null
+++ b/docs/docs/modifiers.md
@@ -0,0 +1,86 @@
+# Modifiers
+
+This group of methods performs helpful modifications to a copy of the current instance.
+You'll notice that the `start_of()`, `next()` and `previous()` methods
+set the time to `00:00:00` and the `end_of()` methods set the time to `23:59:59.999999`.
+
+The only one slightly different is the `average()` method.
+It returns the middle date between itself and the provided `DateTime` argument.
+
+```python
+>>> import pendulum
+
+>>> dt = pendulum.datetime(2012, 1, 31, 12, 0, 0)
+
+>>> dt.start_of('day')
+'2012-01-31 00:00:00'
+
+>>> dt.end_of('day')
+'2012-01-31 23:59:59'
+
+>>> dt.start_of('month')
+'2012-01-01 00:00:00'
+
+>>> dt.end_of('month')
+'2012-01-31 23:59:59'
+
+>>> dt.start_of('year')
+'2012-01-01 00:00:00'
+
+>>> dt.end_of('year')
+'2012-12-31 23:59:59'
+
+>>> dt.start_of('decade')
+'2010-01-01 00:00:00'
+
+>>> dt.end_of('decade')
+'2019-12-31 23:59:59'
+
+>>> dt.start_of('century')
+'2000-01-01 00:00:00'
+
+>>> dt.end_of('century')
+'2099-12-31 23:59:59'
+
+>>> dt.start_of('week')
+'2012-01-30 00:00:00'
+>>> dt.day_of_week == pendulum.MONDAY
+True # ISO8601 week starts on Monday
+
+>>> dt.end_of('week')
+'2012-02-05 23:59:59'
+>>> dt.day_of_week == pendulum.SUNDAY
+True # ISO8601 week ends on SUNDAY
+
+>>> dt.next(pendulum.WEDNESDAY)
+'2012-02-01 00:00:00'
+>>> dt.day_of_week == pendulum.WEDNESDAY
+True
+
+>>> dt = pendulum.datetime(2012, 1, 1, 12, 0, 0)
+dt.next()
+'2012-01-08 00:00:00'
+>>> dt.next(keep_time=True)
+'2012-01-08T12:00:00+00:00'
+
+>>> dt = pendulum.datetime(2012, 1, 31, 12, 0, 0)
+>>> dt.previous(pendulum.WEDNESDAY)
+'2012-01-25 00:00:00'
+>>> dt.day_of_week == pendulum.WEDNESDAY
+True
+
+>>> dt = pendulum.datetime(2012, 1, 1, 12, 0, 0)
+>>> dt.previous()
+'2011-12-25 00:00:00'
+>>> dt.previous(keep_time=True)
+'2011-12-25 12:00:00'
+
+>>> start = pendulum.datetime(2014, 1, 1)
+>>> end = pendulum.datetime(2014, 1, 30)
+>>> start.average(end)
+'2014-01-15 12:00:00'
+
+# others that are defined that are similar
+# and tha accept month, quarter and year units
+# first_of(), last_of(), nth_of()
+```
diff --git a/docs/docs/parsing.md b/docs/docs/parsing.md
new file mode 100644
index 0000000..dd78fd4
--- /dev/null
+++ b/docs/docs/parsing.md
@@ -0,0 +1,114 @@
+# Parsing
+
+The library natively supports the RFC 3339 format, most ISO 8601 formats and some other common formats.
+
+```python
+>>> import pendulum
+
+>>> dt = pendulum.parse('1975-05-21T22:00:00')
+>>> print(dt)
+'1975-05-21T22:00:00+00:00
+
+# You can pass a tz keyword to specify the timezone
+>>> dt = pendulum.parse('1975-05-21T22:00:00', tz='Europe/Paris')
+>>> print(dt)
+'1975-05-21T22:00:00+01:00'
+
+# Not ISO 8601 compliant but common
+>>> dt = pendulum.parse('1975-05-21 22:00:00')
+```
+
+If you pass a non-standard or more complicated string, it will raise an exception, so it is advised to
+use the `from_format()` helper instead.
+
+However, if you want the library to fall back on the [dateutil](https://dateutil.readthedocs.io) parser,
+you have to pass `strict=False`.
+
+```python
+>>> import pendulum
+
+>>> dt = pendulum.parse('31-01-01')
+Traceback (most recent call last):
+...
+ParserError: Unable to parse string [31-01-01]
+
+>>> dt = pendulum.parse('31-01-01', strict=False)
+>>> print(dt)
+'2031-01-01T00:00:00+00:00'
+```
+
+
+## RFC 3339
+
+| String | Output |
+| --------------------------------- | ------------------------------------------|
+| 1996-12-19T16:39:57-08:00 | 1996-12-19T16:39:57-08:00 |
+| 1990-12-31T23:59:59Z | 1990-12-31T23:59:59+00:00 |
+
+## ISO 8601
+
+### Datetime
+
+| String | Output |
+| --------------------------------- | ----------------------------------------- |
+| 20161001T143028+0530 | 2016-10-01T14:30:28+05:30 |
+| 20161001T14 | 2016-10-01T14:00:00+00:00 |
+
+### Date
+
+| String | Output |
+| --------------------------------- | ----------------------------------------- |
+| 2012 | 2012-01-01T00:00:00+00:00 |
+| 2012-05-03 | 2012-05-03T00:00:00+00:00 |
+| 20120503 | 2012-05-03T00:00:00+00:00 |
+| 2012-05 | 2012-05-01T00:00:00+00:00 |
+
+### Ordinal day
+
+| String | Output |
+| ---------------------------------- | ----------------------------------------- |
+| 2012-007 | 2012-01-07T00:00:00+00:00 |
+| 2012007 | 2012-01-07T00:00:00+00:00 |
+
+### Week number
+
+| String | Output |
+| --------------------------------- | ----------------------------------------- |
+| 2012-W05 | 2012-01-30T00:00:00+00:00 |
+| 2012W05 | 2012-01-30T00:00:00+00:00 |
+| 2012-W05-5 | 2012-02-03T00:00:00+00:00 |
+| 2012W055 | 2012-02-03T00:00:00+00:00 |
+
+### Time
+
+When passing only time information the date will default to today.
+
+| String | Output |
+| --------------------------------- | ------------------------------------------ |
+| 00:00 | 2016-12-17T00:00:00+00:00 |
+| 12:04:23 | 2016-12-17T12:04:23+00:00 |
+| 120423 | 2016-12-17T12:04:23+00:00 |
+| 12:04:23.45 | 2016-12-17T12:04:23.450000+00:00 |
+
+### Intervals
+
+| String | Output |
+| ----------------------------------------- | ------------------------------------------------------ |
+| 2007-03-01T13:00:00Z/2008-05-11T15:30:00Z | 2007-03-01T13:00:00+00:00 -> 2008-05-11T15:30:00+00:00 |
+| 2008-05-11T15:30:00Z/P1Y2M10DT2H30M | 2008-05-11T15:30:00+00:00 -> 2009-07-21T18:00:00+00:00 |
+| P1Y2M10DT2H30M/2008-05-11T15:30:00Z | 2007-03-01T13:00:00+00:00 -> 2008-05-11T15:30:00+00:00 |
+
+!!!note
+
+ You can pass the ``exact`` keyword argument to ``parse()`` to get the exact type
+ that the string represents:
+
+ ```python
+ >>> import pendulum
+
+ >>> pendulum.parse('2012-05-03', exact=True)
+ Date(2012, 05, 03)
+
+ >>> pendulum.parse('12:04:23', exact=True)
+ Time(12, 04, 23)
+ ```
diff --git a/docs/docs/period.md b/docs/docs/period.md
new file mode 100644
index 0000000..a4dde16
--- /dev/null
+++ b/docs/docs/period.md
@@ -0,0 +1,168 @@
+# Period
+
+When you subtract a `DateTime` instance from another, or use the `diff()` method, it will return a `Period` instance.
+It inherits from the [Duration](#duration) class with the added benefit that it is aware of the
+instances that generated it, so that it can give access to more methods and properties:
+
+```python
+>>> import pendulum
+
+>>> start = pendulum.datetime(2000, 11, 20)
+>>> end = pendulum.datetime(2016, 11, 5)
+
+>>> period = end - start
+
+>>> period.years
+15
+>>> period.months
+11
+>>> period.in_years()
+15
+>>> period.in_months()
+191
+
+# Note that the weeks property
+# will change compared to the Duration class
+>>> period.weeks
+2 # 832 for the duration
+
+# However the days property will still remain the same
+# to keep the compatibility with the timedelta class
+>>> period.days
+5829
+```
+
+Be aware that a period, just like an interval, is compatible with the `timedelta` class regarding
+its attributes. However, its custom attributes (like `remaining_days`) will be aware of any DST
+transitions that might have occurred and adjust accordingly. Let's take an example:
+
+```python
+>>> import pendulum
+
+>>> start = pendulum.datetime(2017, 3, 7, tz='America/Toronto')
+>>> end = start.add(days=6)
+
+>>> period = end - start
+
+# timedelta properties
+>>> period.days
+5
+>>> period.seconds
+82800
+
+# period properties
+>>> period.remaining_days
+6
+>>> period.hours
+0
+>>> period.remaining_seconds
+0
+```
+
+!!!warning
+
+ Due to their nature (fixed duration between two datetimes), most arithmetic operations will
+ return a `Duration` instead of a `Period`.
+
+ ```python
+ >>> import pendulum
+
+ >>> dt1 = pendulum.datetime(2016, 8, 7, 12, 34, 56)
+ >>> dt2 = dt1.add(days=6, seconds=34)
+ >>> period = pendulum.period(dt1, dt2)
+ >>> period * 2
+ Duration(weeks=1, days=5, minutes=1, seconds=8)
+ ```
+
+
+## Instantiation
+
+You can create an instance by using the `period()` helper:
+
+```python
+
+>>> import pendulum
+
+>>> start = pendulum.datetime(2000, 1, 1)
+>>> end = pendulum.datetime(2000, 1, 31)
+
+>>> period = pendulum.period(start, end)
+```
+
+You can also make an inverted period:
+
+```python
+>>> period = pendulum.period(end, start)
+>>> period.remaining_days
+-2
+```
+
+If you have inverted dates but want to make sure that the period is positive,
+you should set the `absolute` keyword argument to `True`:
+
+```python
+>>> period = pendulum.period(end, start, absolute=True)
+>>> period.remaining_days
+2
+```
+
+## Range
+
+If you want to iterate over a period, you can use the `range()` method:
+
+```python
+>>> import pendulum
+
+>>> start = pendulum.datetime(2000, 1, 1)
+>>> end = pendulum.datetime(2000, 1, 10)
+
+>>> period = pendulum.period(start, end)
+
+>>> for dt in period.range('days'):
+>>> print(dt)
+
+'2000-01-01T00:00:00+00:00'
+'2000-01-02T00:00:00+00:00'
+'2000-01-03T00:00:00+00:00'
+'2000-01-04T00:00:00+00:00'
+'2000-01-05T00:00:00+00:00'
+'2000-01-06T00:00:00+00:00'
+'2000-01-07T00:00:00+00:00'
+'2000-01-08T00:00:00+00:00'
+'2000-01-09T00:00:00+00:00'
+'2000-01-10T00:00:00+00:00'
+```
+
+!!!note
+
+ Supported units for `range()` are: `years`, `months`, `weeks`,
+ `days`, `hours`, `minutes`, `seconds` and `microseconds`
+
+You can pass an amount for the passed unit to control the length of the gap:
+
+```python
+>>> for dt in period.range('days', 2):
+>>> print(dt)
+
+'2000-01-01T00:00:00+00:00'
+'2000-01-03T00:00:00+00:00'
+'2000-01-05T00:00:00+00:00'
+'2000-01-07T00:00:00+00:00'
+'2000-01-09T00:00:00+00:00'
+```
+
+You can also directly iterate over the `Period` instance,
+the unit will be `days` in this case:
+
+```python
+>>> for dt in period:
+>>> print(dt)
+```
+
+You can check if a `DateTime` instance is inside a period using the `in` keyword:
+
+```python
+>>> dt = pendulum.datetime(2000, 1, 4)
+>>> dt in period
+True
+```
diff --git a/docs/docs/string_formatting.md b/docs/docs/string_formatting.md
new file mode 100644
index 0000000..91b95fc
--- /dev/null
+++ b/docs/docs/string_formatting.md
@@ -0,0 +1,175 @@
+# String formatting
+
+The `__str__` magic method is defined to allow `DateTime` instances to be printed
+as a pretty date string when used in a string context.
+
+The default string representation is the same as the one returned by the `isoformat()` method.
+
+```python
+>>> import pendulum
+
+>>> dt = pendulum.datetime(1975, 12, 25, 14, 15, 16)
+>>> print(dt)
+'1975-12-25T14:15:16+00:00'
+
+>>> dt.to_date_string()
+'1975-12-25'
+
+>>> dt.to_formatted_date_string()
+'Dec 25, 1975'
+
+>>> dt.to_time_string()
+'14:15:16'
+
+>>> dt.to_datetime_string()
+'1975-12-25 14:15:16'
+
+>>> dt.to_day_datetime_string()
+'Thu, Dec 25, 1975 2:15 PM'
+
+# You can also use the format() method
+>>> dt.format('dddd Do [of] MMMM YYYY HH:mm:ss A')
+'Thursday 25th of December 1975 02:15:16 PM'
+
+# Of course, the strftime method is still available
+>>> dt.strftime('%A %-d%t of %B %Y %I:%M:%S %p')
+'Thursday 25th of December 1975 02:15:16 PM'
+```
+
+!!!note
+
+ For localization support see the [Localization](#localization) section.
+
+## Common Formats
+
+
+The following are methods to display a `DateTime` instance as a common format:
+
+```python
+>>> import pendulum
+
+>>> dt = pendulum.now()
+
+>>> dt.to_atom_string()
+'1975-12-25T14:15:16-05:00'
+
+>>> dt.to_cookie_string()
+'Thursday, 25-Dec-1975 14:15:16 EST'
+
+>>> dt.to_iso8601_string()
+'1975-12-25T14:15:16-0500'
+
+>>> dt.to_rfc822_string()
+'Thu, 25 Dec 75 14:15:16 -0500'
+
+>>> dt.to_rfc850_string()
+'Thursday, 25-Dec-75 14:15:16 EST'
+
+>>> dt.to_rfc1036_string()
+'Thu, 25 Dec 75 14:15:16 -0500'
+
+>>> dt.to_rfc1123_string()
+'Thu, 25 Dec 1975 14:15:16 -0500'
+
+>>> dt.to_rfc2822_string()
+'Thu, 25 Dec 1975 14:15:16 -0500'
+
+>>> dt.to_rfc3339_string()
+'1975-12-25T14:15:16-05:00'
+
+>>> dt.to_rss_string()
+'Thu, 25 Dec 1975 14:15:16 -0500'
+
+>>> dt.to_w3c_string()
+'1975-12-25T14:15:16-05:00'
+```
+
+## Formatter
+
+Pendulum uses its own formatter when using the `format()` method.
+
+This format is more intuitive to use than the one used with `strftime()`
+and supports more directives.
+
+```python
+>>> import pendulum
+
+>>> dt = pendulum.datetime(1975, 12, 25, 14, 15, 16)
+>>> dt.format('YYYY-MM-DD HH:mm:ss')
+'1975-12-25 14:15:16'
+```
+
+### Tokens
+
+The following tokens are currently supported:
+
+
+| | Token | Output |
+| ------------------------------ | ------------- | ------------------------------------------ |
+| **Year** | YYYY | 2000, 2001, 2002 ... 2012, 2013 |
+| | YY | 00, 01, 02 ... 12, 13 |
+| | Y | 2000, 2001, 2002 ... 2012, 2013 |
+| **Quarter** | Q | 1 2 3 4 |
+| | Qo | 1st 2nd 3rd 4th |
+| **Month** | MMMM | January, February, March ... |
+| | MMM | Jan, Feb, Mar ... |
+| | MM | 01, 02, 03 ... 11, 12 |
+| | M | 1, 2, 3 ... 11, 12 |
+| | Mo | 1st 2nd ... 11th 12th |
+| **Day of Year** | DDDD | 001, 002, 003 ... 364, 365 |
+| | DDD | 1, 2, 3 ... 4, 5 |
+| **Day of Month** | DD | 01, 02, 03 ... 30, 31 |
+| | D | 1, 2, 3 ... 30, 31 |
+| | Do | 1st, 2nd, 3rd ... 30th, 31st |
+| **Day of Week** | dddd | Monday, Tuesday, Wednesday ... |
+| | ddd | Mon, Tue, Wed ... |
+| | dd | Mo, Tu, We ... |
+| | d | 0, 1, 2 ... 6 |
+| **Days of ISO Week** | E | 1, 2, 3 ... 7 |
+| **Hour** | HH | 00, 01, 02 ... 23 |
+| | H | 0, 1, 2 ... 23 |
+| | hh | 01, 02, 03 ... 11, 12 |
+| | h | 1, 2, 3 ... 11, 12 |
+| **Minute** | mm | 00, 01, 02 ... 58, 59 |
+| | m | 0, 1, 2 ... 58, 59 |
+| **Second** | ss | 00, 01, 02 ... 58, 59 |
+| | s | 0, 1, 2 ... 58, 59 |
+| **Fractional Second** | S | 0 1 ... 8 9 |
+| | SS | 00, 01, 02 ... 98, 99 |
+| | SSS | 000 001 ... 998 999 |
+| | SSSS ... | 000[0..] 001[0..] ... 998[0..] 999[0..] |
+| | SSSSSS | |
+| **AM / PM** | A | AM, PM |
+| **Timezone** | Z | -07:00, -06:00 ... +06:00, +07:00 |
+| | ZZ | -0700, -0600 ... +0600, +0700 |
+| | z | Asia/Baku, Europe/Warsaw, GMT ... |
+| | zz | EST CST ... MST PST |
+| **Seconds timestamp** | X | 1381685817, 1234567890.123 |
+| **Milliseconds timestamp** | x | 1234567890123 |
+
+
+### Localized Formats
+
+Because preferred formatting differs based on locale,
+there are a few tokens that can be used to format an instance based on its locale.
+
+| | | |
+| ------------------------------------------------------ | ------------- | ------------------------------------------ |
+| **Time** | LT | 8:30 PM |
+| **Time with seconds** | LTS | 8:30:25 PM |
+| **Month numeral, day of month, year** | L | 09/04/1986 |
+| **Month name, day of month, year** | LL | September 4 1986 |
+| **Month name, day of month, year, time** | LLL | September 4 1986 8:30 PM |
+| **Month name, day of month, day of week, year, time** | LLLL | Thursday, September 4 1986 8:30 PM |
+
+### Escaping Characters
+
+To escape characters in format strings, you can wrap the characters in square brackets.
+
+```python
+>>> import pendulum
+
+>>> dt = pendulum.now()
+>>> dt.format('[today] dddd')
+'today Sunday'
+```
diff --git a/docs/docs/testing.md b/docs/docs/testing.md
new file mode 100644
index 0000000..dfca054
--- /dev/null
+++ b/docs/docs/testing.md
@@ -0,0 +1,59 @@
+# Testing
+
+The testing methods allow you to set a `DateTime` instance (real or mock) to be returned
+when a "now" instance is created.
+The provided instance will be returned specifically under the following conditions:
+
+* A call to the `now()` method, ex. `pendulum.now()`.
+* When the string "now" is passed to the `parse()` method, ex. `pendulum.parse('now')`
+
+```python
+>>> import pendulum
+
+# Create testing datetime
+>>> known = pendulum.datetime(2001, 5, 21, 12)
+
+# Set the mock
+>>> pendulum.set_test_now(known)
+
+>>> print(pendulum.now())
+'2001-05-21T12:00:00+00:00'
+
+>>> print(pendulum.parse('now'))
+'2001-05-21T12:00:00+00:00'
+
+# Clear the mock
+>>> pendulum.set_test_now()
+
+>>> print(pendulum.now())
+'2016-07-10T22:10:33.954851-05:00'
+```
+
+Related methods will also return values mocked according to the **now** instance.
+
+```python
+>>> print(pendulum.today())
+'2001-05-21T00:00:00+00:00'
+
+>>> print(pendulum.tomorrow())
+'2001-05-22T00:00:00+00:00'
+
+>>> print(pendulum.yesterday())
+'2001-05-20T00:00:00+00:00'
+```
+
+If you don't want to manually clear the mock (or you are afraid of forgetting),
+you can use the provided `test()` contextmanager.
+
+```python
+>>> import pendulum
+
+>>> known = pendulum.datetime(2001, 5, 21, 12)
+
+>>> with pendulum.test(known):
+>>> print(pendulum.now())
+'2001-05-21T12:00:00+00:00'
+
+>>> print(pendulum.now())
+'2016-07-10T22:10:33.954851-05:00'
+```
diff --git a/docs/docs/timezones.md b/docs/docs/timezones.md
new file mode 100644
index 0000000..85ff147
--- /dev/null
+++ b/docs/docs/timezones.md
@@ -0,0 +1,193 @@
+# Timezones
+
+Timezones are an important part of every datetime library, and `pendulum`
+tries to provide an easy and accurate system to handle them properly.
+
+!!!note
+
+ The timezone system works best inside the `pendulum` ecosystem but
+ can also be used with the standard ``datetime`` library with a few limitations.
+ See [Using the timezone library directly](#using-the-timezone-library-directly).
+
+## Normalization
+
+When you create a `DateTime` instance, the library will normalize it for the
+given timezone to properly handle any transition that might have occurred.
+
+```python
+>>> import pendulum
+
+>>> pendulum.datetime(2013, 3, 31, 2, 30, tz='Europe/Paris')
+# 2:30 for the 31th of March 2013 does not exist
+# so pendulum will return the actual time which is 3:30+02:00
+'2013-03-31T03:30:00+02:00'
+
+>>> pendulum.datetime(2013, 10, 27, 2, 30, tz='Europe/Paris')
+# Here, 2:30 exists twice in the day so pendulum will
+# assume that the transition already occurred
+'2013-10-27T02:30:00+01:00'
+```
+
+You can, however, control the normalization behavior:
+
+```python
+>>> import pendulum
+
+>>> pendulum.datetime(2013, 3, 31, 2, 30, 0, 0, tz='Europe/Paris',
+ dst_rule=pendulum.PRE_TRANSITION)
+'2013-03-31T01:30:00+01:00'
+>>> pendulum.datetime(2013, 10, 27, 2, 30, 0, 0, tz='Europe/Paris',
+ dst_rule=pendulum.PRE_TRANSITION)
+'2013-10-27T02:30:00+02:00'
+
+>>> pendulum.datetime(2013, 3, 31, 2, 30, 0, 0, tz='Europe/Paris',
+ dst_rule=pendulum.TRANSITION_ERROR)
+# NonExistingTime: The datetime 2013-03-31 02:30:00 does not exist
+>>> pendulum.datetime(2013, 10, 27, 2, 30, 0, 0, tz='Europe/Paris',
+ dst_rule=pendulum.TRANSITION_ERROR)
+# AmbiguousTime: The datetime 2013-10-27 02:30:00 is ambiguous.
+```
+
+Note that it only affects instances at creation time. Shifting time around
+transition times still behaves the same.
+
+## Shifting time to transition
+
+So, what happens when you add time to a `DateTime` instance and stumble upon
+a transition time?
+Well `pendulum`, provided with the context of the previous instance, will
+adopt the proper behavior and apply the transition accordingly.
+
+```python
+>>> import pendulum
+
+>>> dt = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999,
+ tz='Europe/Paris')
+'2013-03-31T01:59:59.999999+01:00'
+>>> dt = dt.add(microseconds=1)
+'2013-03-31T03:00:00+02:00'
+>>> dt.subtract(microseconds=1)
+'2013-03-31T01:59:59.999998+01:00'
+
+>>> dt = pendulum.datetime(2013, 10, 27, 2, 59, 59, 999999,
+ tz='Europe/Paris',
+ dst_rule=pendulum.PRE_TRANSITION)
+'2013-10-27T02:59:59.999999+02:00'
+>>> dt = dt.add(microseconds=1)
+'2013-10-27T02:00:00+01:00'
+>>> dt = dt.subtract(microseconds=1)
+'2013-10-27T02:59:59.999999+02:00'
+```
+
+## Switching timezones
+
+You can easily change the timezone of a `DateTime` instance
+with the `in_timezone()` method.
+
+!!!note
+
+ You can also use the more concise ``in_tz()``
+
+```python
+>>> in_paris = pendulum.datetime(2016, 8, 7, 22, 24, 30, tz='Europe/Paris')
+'2016-08-07T22:24:30+02:00'
+>>> in_paris.in_timezone('America/New_York')
+'2016-08-07T16:24:30-04:00'
+>>> in_paris.in_tz('Asia/Tokyo')
+'2016-08-08T05:24:30+09:00'
+```
+
+## Using the timezone library directly
+
+!!!warning
+
+ **You should avoid using the timezone library in Python < 3.6.**
+
+ This is due to the fact that Pendulum relies heavily on the presence
+ of the `fold` attribute which was introduced in Python 3.6.
+
+ The reason it works inside the Pendulum ecosystem is that it
+ backports the `fold` attribute in the `DateTime` class.
+
+Like said in the introduction, you can use the timezone library
+directly with standard `datetime` objects but with limitations, especially
+when adding and subtracting time around transition times.
+
+The value of the `fold` attribute will be used
+by default to determine the transition rule.
+
+```python
+>>> from datetime import datetime
+>>> from pendulum import timezone
+
+>>> paris = timezone('Europe/Paris')
+>>> dt = datetime(2013, 3, 31, 2, 30)
+# By default, fold is set to 0
+>>> dt = paris.convert(dt)
+>>> dt.isoformat()
+'2013-03-31T01:30:00+01:00'
+
+>>> dt = datetime(2013, 3, 31, 2, 30, fold=1)
+>>> dt = paris.convert(dt)
+>>> dt.isoformat()
+'2013-03-31T03:30:00+02:00'
+```
+
+Instead of relying on the `fold` attribute, you can use the `dst_rule`
+keyword argument. This is especially useful if you want to raise errors
+on non-existing and ambiguous times.
+
+```python
+>>> import pendulum
+
+>>> dt = datetime(2013, 3, 31, 2, 30)
+# By default, fold is set to 0
+>>> dt = paris.convert(dt, dst_rule=pendulum.PRE_TRANSITION)
+>>> dt.isoformat()
+'2013-03-31T01:30:00+01:00'
+
+>>> dt = paris.convert(dt, dst_rule=pendulum.POST_TRANSITION)
+>>> dt.isoformat()
+'2013-03-31T03:30:00+02:00'
+
+>>> paris.convert(dt, dst_rule=pendulum.TRANSITION_ERROR)
+# NonExistingTime: The datetime 2013-03-31 02:30:00 does not exist
+```
+
+This works as expected. However, whenever we add or subtract a `timedelta`
+object, things get tricky.
+
+```python
+>>> from datetime import datetime, timedelta
+>>> from pendulum import timezone
+
+>>> dt = datetime(2013, 3, 31, 1, 59, 59, 999999)
+>>> dt = paris.convert(dt)
+>>> dt.isoformat()
+'2013-03-31T01:59:59.999999+01:00'
+>>> dt = dt + timedelta(microseconds=1)
+>>> dt.isoformat()
+'2013-03-31T02:00:00+01:00'
+```
+
+This is not what we expect. It should be `2013-03-31T03:00:00+02:00`.
+It is actually easy to retrieve the proper datetime by using `convert()`
+again.
+
+```python
+>>> dt = tz.convert(dt)
+>>> dt.isoformat()
+'2013-03-31T03:00:00+02:00'
+```
+
+You can also get a normalized `datetime` object
+from a `Timezone` by using the `datetime()` method:
+
+```python
+>>> import pendulum
+
+>>> tz = pendulum.timezone('Europe/Paris')
+>>> dt = tz.datetime(2013, 3, 31, 2, 30)
+>>> dt.isoformat()
+'2013-03-31T03:30:00+02:00'
+```
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
new file mode 100644
index 0000000..da64d75
--- /dev/null
+++ b/docs/mkdocs.yml
@@ -0,0 +1,17 @@
+site_name: Pendulum documentation
+
+theme:
+ name: null
+ custom_dir: theme
+
+extra:
+ version: 2.1
+
+markdown_extensions:
+ - codehilite
+ - admonition
+ - pymdownx.superfences
+ - toc:
+ permalink: 
+ - markdown_include.include:
+ base_path: docs
diff --git a/docs/theme/main.html b/docs/theme/main.html
new file mode 100644
index 0000000..80caadf
--- /dev/null
+++ b/docs/theme/main.html
@@ -0,0 +1,33 @@
+---
+layout: documentation
+title: {{ config.site_name|striptags|e }}
+version: 2.x
+---
+
+
+ <!-- START CONTENT SECTION -->
+ <section class="sm-p-b-30 p-b-70 p-t-100">
+ <div class="container">
+ <div class="row">
+ <div class="col-sm-6 col-sm-offset-3 text-center p-t-100">
+ <h6 class="block-title m-b-0 hint-text">Version <span class="documentation-version">{{config.extra.version}}</span></h6>
+ <h1 class="m-t-5">Documentation</h1>
+ </div>
+ </div>
+ </div>
+ </section>
+ <!-- END CONTENT SECTION -->
+
+ <section class="p-b-50 p-t-50 documentation-content">
+ <div class="container">
+ <div class="panel panel-transparent">
+ <div class="bg-white row p-l-20 p-r-20 p-b-20 p-t-5 xs-no-padding">
+ <div class="row">
+ <div class="col-md-8 col-md-offset-2 documentation-body">
+ {{page.content}}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..666c281
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,20 @@
+project('pendulum C extensions', 'c')
+
+py_mod = import('python')
+py = py_mod.find_installation()
+py_dep = py.dependency()
+
+extensions = [
+ ['_helpers', 'pendulum/_extensions/_helpers.c', meson.source_root() / 'pendulum/_extensions/'],
+ ['_iso8601', 'pendulum/parsing/_iso8601.c', meson.source_root() / 'pendulum/parsing/'],
+]
+
+foreach extension : extensions
+ py.extension_module(
+ extension[0],
+ extension[1],
+ dependencies : py_dep,
+ install : true,
+ install_dir: extension[2]
+ )
+endforeach
diff --git a/pendulum/__init__.py b/pendulum/__init__.py
index a85ed88..4491244 100644
--- a/pendulum/__init__.py
+++ b/pendulum/__init__.py
@@ -1,315 +1,363 @@
-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)
+from __future__ import annotations
+
+import datetime as _datetime
+
+from typing import Union
+from typing import cast
+
+from pendulum.__version__ import __version__
+from pendulum.constants import DAYS_PER_WEEK
+from pendulum.constants import FRIDAY
+from pendulum.constants import HOURS_PER_DAY
+from pendulum.constants import MINUTES_PER_HOUR
+from pendulum.constants import MONDAY
+from pendulum.constants import MONTHS_PER_YEAR
+from pendulum.constants import SATURDAY
+from pendulum.constants import SECONDS_PER_DAY
+from pendulum.constants import SECONDS_PER_HOUR
+from pendulum.constants import SECONDS_PER_MINUTE
+from pendulum.constants import SUNDAY
+from pendulum.constants import THURSDAY
+from pendulum.constants import TUESDAY
+from pendulum.constants import WEDNESDAY
+from pendulum.constants import WEEKS_PER_YEAR
+from pendulum.constants import YEARS_PER_CENTURY
+from pendulum.constants import YEARS_PER_DECADE
+from pendulum.date import Date
+from pendulum.datetime import DateTime
+from pendulum.duration import Duration
+from pendulum.formatting import Formatter
+from pendulum.helpers import format_diff
+from pendulum.helpers import get_locale
+from pendulum.helpers import locale
+from pendulum.helpers import set_locale
+from pendulum.helpers import week_ends_at
+from pendulum.helpers import week_starts_at
+from pendulum.interval import Interval
+from pendulum.parser import parse
+from pendulum.testing.traveller import Traveller
+from pendulum.time import Time
+from pendulum.tz import UTC
+from pendulum.tz import local_timezone
+from pendulum.tz import set_local_timezone
+from pendulum.tz import test_local_timezone
+from pendulum.tz import timezone
+from pendulum.tz import timezones
+from pendulum.tz.timezone import FixedTimezone
+from pendulum.tz.timezone import Timezone
+
+_TEST_NOW: DateTime | None = None
+_LOCALE = "en"
+_WEEK_STARTS_AT = MONDAY
+_WEEK_ENDS_AT = SUNDAY
+
+_formatter = Formatter()
+
+
+def _safe_timezone(
+ obj: str | float | _datetime.tzinfo | Timezone | FixedTimezone | None,
+ dt: _datetime.datetime | None = None,
+) -> Timezone | FixedTimezone:
+ """
+ Creates a timezone instance
+ from a string, Timezone, TimezoneInfo or integer offset.
+ """
+ if isinstance(obj, (Timezone, FixedTimezone)):
+ return obj
+
+ if obj is None or obj == "local":
+ return local_timezone()
+
+ if isinstance(obj, (int, float)):
+ obj = int(obj * 60 * 60)
+ elif isinstance(obj, _datetime.tzinfo):
+ # zoneinfo
+ if hasattr(obj, "key"):
+ obj = obj.key # type: ignore
+ # pytz
+ elif hasattr(obj, "localize"):
+ obj = obj.zone # type: ignore
+ elif obj.tzname(None) == "UTC":
+ return UTC
+ else:
+ offset = obj.utcoffset(dt)
+
+ if offset is None:
+ offset = _datetime.timedelta(0)
+
+ obj = int(offset.total_seconds())
+
+ obj = cast(Union[str, int], obj)
+
+ return timezone(obj)
+
+
+# Public API
+def datetime(
+ year: int,
+ month: int,
+ day: int,
+ hour: int = 0,
+ minute: int = 0,
+ second: int = 0,
+ microsecond: int = 0,
+ tz: str | float | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC,
+ fold: int = 1,
+ raise_on_unknown_times: bool = False,
+) -> DateTime:
+ """
+ Creates a new DateTime instance from a specific date and time.
+ """
+ return DateTime.create(
+ year,
+ month,
+ day,
+ hour=hour,
+ minute=minute,
+ second=second,
+ microsecond=microsecond,
+ tz=tz,
+ fold=fold,
+ raise_on_unknown_times=raise_on_unknown_times,
+ )
+
+
+def local(
+ year: int,
+ month: int,
+ day: int,
+ hour: int = 0,
+ minute: int = 0,
+ second: int = 0,
+ microsecond: int = 0,
+) -> DateTime:
+ """
+ Return a DateTime in the local timezone.
+ """
+ return datetime(
+ year, month, day, hour, minute, second, microsecond, tz=local_timezone()
+ )
+
+
+def naive(
+ year: int,
+ month: int,
+ day: int,
+ hour: int = 0,
+ minute: int = 0,
+ second: int = 0,
+ microsecond: int = 0,
+ fold: int = 1,
+) -> DateTime:
+ """
+ Return a naive DateTime.
+ """
+ return DateTime(year, month, day, hour, minute, second, microsecond, fold=fold)
+
+
+def date(year: int, month: int, day: int) -> Date:
+ """
+ Create a new Date instance.
+ """
+ return Date(year, month, day)
+
+
+def time(hour: int, minute: int = 0, second: int = 0, microsecond: int = 0) -> Time:
+ """
+ Create a new Time instance.
+ """
+ return Time(hour, minute, second, microsecond)
+
+
+def instance(
+ dt: _datetime.datetime,
+ tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC,
+) -> 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
+
+ if tz is not None:
+ tz = _safe_timezone(tz, dt=dt)
+
+ return datetime(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ tz=cast(Union[str, int, Timezone, FixedTimezone, None], tz),
+ )
+
+
+def now(tz: str | Timezone | None = None) -> DateTime:
+ """
+ Get a DateTime instance for the current date and time.
+ """
+ return DateTime.now(tz)
+
+
+def today(tz: str | Timezone = "local") -> DateTime:
+ """
+ Create a DateTime instance for today.
+ """
+ return now(tz).start_of("day")
+
+
+def tomorrow(tz: str | Timezone = "local") -> DateTime:
+ """
+ Create a DateTime instance for today.
+ """
+ return today(tz).add(days=1)
+
+
+def yesterday(tz: str | Timezone = "local") -> DateTime:
+ """
+ Create a DateTime instance for today.
+ """
+ return today(tz).subtract(days=1)
+
+
+def from_format(
+ string: str,
+ fmt: str,
+ tz: str | Timezone = UTC,
+ locale: str | None = None,
+) -> DateTime:
+ """
+ Creates a DateTime instance from a specific format.
+ """
+ parts = _formatter.parse(string, fmt, now(tz=tz), locale=locale)
+ if parts["tz"] is None:
+ parts["tz"] = tz
+
+ return datetime(**parts)
+
+
+def from_timestamp(timestamp: int | float, tz: str | Timezone = UTC) -> DateTime:
+ """
+ Create a DateTime instance from a timestamp.
+ """
+ dt = _datetime.datetime.utcfromtimestamp(timestamp)
+
+ dt = datetime(
+ dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond
+ )
+
+ if tz is not UTC or tz != "UTC":
+ dt = dt.in_timezone(tz)
+
+ return dt
+
+
+def duration(
+ days: float = 0,
+ seconds: float = 0,
+ microseconds: float = 0,
+ milliseconds: float = 0,
+ minutes: float = 0,
+ hours: float = 0,
+ weeks: float = 0,
+ years: float = 0,
+ months: float = 0,
+) -> Duration:
+ """
+ Create a Duration instance.
+ """
+ return Duration(
+ days=days,
+ seconds=seconds,
+ microseconds=microseconds,
+ milliseconds=milliseconds,
+ minutes=minutes,
+ hours=hours,
+ weeks=weeks,
+ years=years,
+ months=months,
+ )
+
+
+def interval(start: DateTime, end: DateTime, absolute: bool = False) -> Interval:
+ """
+ Create an Interval instance.
+ """
+ return Interval(start, end, absolute=absolute)
+
+
+# Testing
+
+_traveller = Traveller(DateTime)
+
+freeze = _traveller.freeze
+travel = _traveller.travel
+travel_to = _traveller.travel_to
+travel_back = _traveller.travel_back
+
+__all__ = [
+ "__version__",
+ "DAYS_PER_WEEK",
+ "FRIDAY",
+ "HOURS_PER_DAY",
+ "MINUTES_PER_HOUR",
+ "MONDAY",
+ "MONTHS_PER_YEAR",
+ "SATURDAY",
+ "SECONDS_PER_DAY",
+ "SECONDS_PER_HOUR",
+ "SECONDS_PER_MINUTE",
+ "SUNDAY",
+ "THURSDAY",
+ "TUESDAY",
+ "WEDNESDAY",
+ "WEEKS_PER_YEAR",
+ "YEARS_PER_CENTURY",
+ "YEARS_PER_DECADE",
+ "Date",
+ "DateTime",
+ "Duration",
+ "Formatter",
+ "date",
+ "datetime",
+ "duration",
+ "format_diff",
+ "freeze",
+ "from_format",
+ "from_timestamp",
+ "get_locale",
+ "instance",
+ "interval",
+ "local",
+ "locale",
+ "naive",
+ "now",
+ "set_locale",
+ "week_ends_at",
+ "week_starts_at",
+ "parse",
+ "Interval",
+ "Time",
+ "UTC",
+ "local_timezone",
+ "set_local_timezone",
+ "test_local_timezone",
+ "time",
+ "timezone",
+ "timezones",
+ "today",
+ "tomorrow",
+ "travel",
+ "travel_back",
+ "travel_to",
+ "FixedTimezone",
+ "Timezone",
+ "yesterday",
+]
diff --git a/pendulum/__version__.py b/pendulum/__version__.py
index 62b3483..29e86a8 100644
--- a/pendulum/__version__.py
+++ b/pendulum/__version__.py
@@ -1 +1 @@
-__version__ = "2.1.2"
+__version__ = "3.0.0a"
diff --git a/pendulum/_extensions/_helpers.c b/pendulum/_extensions/_helpers.c
index aa92ae5..a3114d9 100644
--- a/pendulum/_extensions/_helpers.c
+++ b/pendulum/_extensions/_helpers.c
@@ -1,930 +1,931 @@
-/* ------------------------------------------------------------------------- */
-
-#include <Python.h>
-#include <datetime.h>
-#include <structmember.h>
-#include <math.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <time.h>
-
-/* ------------------------------------------------------------------------- */
-
-#define EPOCH_YEAR 1970
-
-#define DAYS_PER_N_YEAR 365
-#define DAYS_PER_L_YEAR 366
-
-#define USECS_PER_SEC 1000000
-
-#define SECS_PER_MIN 60
-#define SECS_PER_HOUR (60 * SECS_PER_MIN)
-#define SECS_PER_DAY (SECS_PER_HOUR * 24)
-
-// 400-year chunks always have 146097 days (20871 weeks).
-#define DAYS_PER_400_YEARS 146097L
-#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)SECS_PER_DAY)
-
-// The number of seconds in an aligned 100-year chunk, for those that
-// do not begin with a leap year and those that do respectively.
-const int64_t SECS_PER_100_YEARS[2] = {
- (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY,
- (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * DAYS_PER_L_YEAR) * SECS_PER_DAY};
-
-// The number of seconds in an aligned 4-year chunk, for those that
-// do not begin with a leap year and those that do respectively.
-const int32_t SECS_PER_4_YEARS[2] = {
- (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY,
- (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY};
-
-// The number of seconds in non-leap and leap years respectively.
-const int32_t SECS_PER_YEAR[2] = {
- DAYS_PER_N_YEAR * SECS_PER_DAY,
- DAYS_PER_L_YEAR *SECS_PER_DAY};
-
-#define MONTHS_PER_YEAR 12
-
-// The month lengths in non-leap and leap years respectively.
-const int32_t DAYS_PER_MONTHS[2][13] = {
- {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
- {-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};
-
-// The day offsets of the beginning of each (1-based) month in non-leap
-// and leap years respectively.
-// For example, in a leap year there are 335 days before December.
-const int32_t MONTHS_OFFSETS[2][14] = {
- {-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365},
- {-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}};
-
-const int DAY_OF_WEEK_TABLE[12] = {
- 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
-
-#define TM_SUNDAY 0
-#define TM_MONDAY 1
-#define TM_TUESDAY 2
-#define TM_WEDNESDAY 3
-#define TM_THURSDAY 4
-#define TM_FRIDAY 5
-#define TM_SATURDAY 6
-
-#define TM_JANUARY 0
-#define TM_FEBRUARY 1
-#define TM_MARCH 2
-#define TM_APRIL 3
-#define TM_MAY 4
-#define TM_JUNE 5
-#define TM_JULY 6
-#define TM_AUGUST 7
-#define TM_SEPTEMBER 8
-#define TM_OCTOBER 9
-#define TM_NOVEMBER 10
-#define TM_DECEMBER 11
-
-/* ------------------------------------------------------------------------- */
-
-int _p(int y)
-{
- return y + y / 4 - y / 100 + y / 400;
-}
-
-int _is_leap(int year)
-{
- return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
-}
-
-int _is_long_year(int year)
-{
- return (_p(year) % 7 == 4) || (_p(year - 1) % 7 == 3);
-}
-
-int _week_day(int year, int month, int day)
-{
- int y;
- int w;
-
- y = year - (month < 3);
-
- w = (_p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7;
-
- if (!w)
- {
- w = 7;
- }
-
- return w;
-}
-
-int _days_in_year(int year)
-{
- if (_is_leap(year))
- {
- return DAYS_PER_L_YEAR;
- }
-
- return DAYS_PER_N_YEAR;
-}
-
-int _day_number(int year, int month, int day)
-{
- month = (month + 9) % 12;
- year = year - month / 10;
-
- return (
- 365 * year + year / 4 - year / 100 + year / 400 + (month * 306 + 5) / 10 + (day - 1));
-}
-
-int _get_offset(PyObject *dt)
-{
- PyObject *tzinfo;
- PyObject *offset;
-
- tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo;
-
- if (tzinfo != Py_None)
- {
- offset = PyObject_CallMethod(tzinfo, "utcoffset", "O", dt);
-
- return PyDateTime_DELTA_GET_DAYS(offset) * SECS_PER_DAY + PyDateTime_DELTA_GET_SECONDS(offset);
- }
-
- return 0;
-}
-
-int _has_tzinfo(PyObject *dt)
-{
- return ((_PyDateTime_BaseTZInfo *)(dt))->hastzinfo;
-}
-
-char *_get_tz_name(PyObject *dt)
-{
- PyObject *tzinfo;
- char *tz = "";
-
- tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo;
-
- if (tzinfo != Py_None)
- {
- if (PyObject_HasAttrString(tzinfo, "name"))
- {
- // Pendulum timezone
- tz = (char *)PyUnicode_AsUTF8(
- PyObject_GetAttrString(tzinfo, "name"));
- }
- else if (PyObject_HasAttrString(tzinfo, "zone"))
- {
- // pytz timezone
- tz = (char *)PyUnicode_AsUTF8(
- PyObject_GetAttrString(tzinfo, "zone"));
- }
- }
-
- return tz;
-}
-
-/* ------------------------ Custom Types ------------------------------- */
-
-/*
- * class Diff():
- */
-typedef struct
-{
- PyObject_HEAD int years;
- int months;
- int days;
- int hours;
- int minutes;
- int seconds;
- int microseconds;
- int total_days;
-} Diff;
-
-/*
- * def __init__(self, years, months, days, hours, minutes, seconds, microseconds, total_days):
- * self.years = years
- * # ...
-*/
-static int Diff_init(Diff *self, PyObject *args, PyObject *kwargs)
-{
- int years;
- int months;
- int days;
- int hours;
- int minutes;
- int seconds;
- int microseconds;
- int total_days;
-
- if (!PyArg_ParseTuple(args, "iiiiiii", &years, &months, &days, &hours, &minutes, &seconds, &microseconds, &total_days))
- return -1;
-
- self->years = years;
- self->months = months;
- self->days = days;
- self->hours = hours;
- self->minutes = minutes;
- self->seconds = seconds;
- self->microseconds = microseconds;
- self->total_days = total_days;
-
- return 0;
-}
-
-/*
- * def __repr__(self):
- * return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format(
- * self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds
- * )
- */
-static PyObject *Diff_repr(Diff *self)
-{
- char repr[82] = {0};
-
- sprintf(
- repr,
- "%d years %d months %d days %d hours %d minutes %d seconds %d microseconds",
- self->years,
- self->months,
- self->days,
- self->hours,
- self->minutes,
- self->seconds,
- self->microseconds);
-
- return PyUnicode_FromString(repr);
-}
-
-/*
- * Instantiate new Diff_type object
- * Skip overhead of calling PyObject_New and PyObject_Init.
- * Directly allocate object.
- */
-static PyObject *new_diff_ex(int years, int months, int days, int hours, int minutes, int seconds, int microseconds, int total_days, PyTypeObject *type)
-{
- Diff *self = (Diff *)(type->tp_alloc(type, 0));
-
- if (self != NULL)
- {
- self->years = years;
- self->months = months;
- self->days = days;
- self->hours = hours;
- self->minutes = minutes;
- self->seconds = seconds;
- self->microseconds = microseconds;
- self->total_days = total_days;
- }
-
- return (PyObject *)self;
-}
-
-/*
- * Class member / class attributes
- */
-static PyMemberDef Diff_members[] = {
- {"years", T_INT, offsetof(Diff, years), 0, "years in diff"},
- {"months", T_INT, offsetof(Diff, months), 0, "months in diff"},
- {"days", T_INT, offsetof(Diff, days), 0, "days in diff"},
- {"hours", T_INT, offsetof(Diff, hours), 0, "hours in diff"},
- {"minutes", T_INT, offsetof(Diff, minutes), 0, "minutes in diff"},
- {"seconds", T_INT, offsetof(Diff, seconds), 0, "seconds in diff"},
- {"microseconds", T_INT, offsetof(Diff, microseconds), 0, "microseconds in diff"},
- {"total_days", T_INT, offsetof(Diff, total_days), 0, "total days in diff"},
- {NULL}};
-
-static PyTypeObject Diff_type = {
- PyVarObject_HEAD_INIT(NULL, 0) "PreciseDiff", /* tp_name */
- sizeof(Diff), /* tp_basicsize */
- 0, /* tp_itemsize */
- 0, /* tp_dealloc */
- 0, /* tp_print */
- 0, /* tp_getattr */
- 0, /* tp_setattr */
- 0, /* tp_as_async */
- (reprfunc)Diff_repr, /* tp_repr */
- 0, /* tp_as_number */
- 0, /* tp_as_sequence */
- 0, /* tp_as_mapping */
- 0, /* tp_hash */
- 0, /* tp_call */
- (reprfunc)Diff_repr, /* tp_str */
- 0, /* tp_getattro */
- 0, /* tp_setattro */
- 0, /* tp_as_buffer */
- Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
- "Precise difference between two datetime objects", /* tp_doc */
-};
-
-#define new_diff(years, months, days, hours, minutes, seconds, microseconds, total_days) new_diff_ex(years, months, days, hours, minutes, seconds, microseconds, total_days, &Diff_type)
-
-/* -------------------------- Functions --------------------------*/
-
-PyObject *is_leap(PyObject *self, PyObject *args)
-{
- PyObject *leap;
- int year;
-
- if (!PyArg_ParseTuple(args, "i", &year))
- {
- PyErr_SetString(
- PyExc_ValueError, "Invalid parameters");
- return NULL;
- }
-
- leap = PyBool_FromLong(_is_leap(year));
-
- return leap;
-}
-
-PyObject *is_long_year(PyObject *self, PyObject *args)
-{
- PyObject *is_long;
- int year;
-
- if (!PyArg_ParseTuple(args, "i", &year))
- {
- PyErr_SetString(
- PyExc_ValueError, "Invalid parameters");
- return NULL;
- }
-
- is_long = PyBool_FromLong(_is_long_year(year));
-
- return is_long;
-}
-
-PyObject *week_day(PyObject *self, PyObject *args)
-{
- PyObject *wd;
- int year;
- int month;
- int day;
-
- if (!PyArg_ParseTuple(args, "iii", &year, &month, &day))
- {
- PyErr_SetString(
- PyExc_ValueError, "Invalid parameters");
- return NULL;
- }
-
- wd = PyLong_FromLong(_week_day(year, month, day));
-
- return wd;
-}
-
-PyObject *days_in_year(PyObject *self, PyObject *args)
-{
- PyObject *ndays;
- int year;
-
- if (!PyArg_ParseTuple(args, "i", &year))
- {
- PyErr_SetString(
- PyExc_ValueError, "Invalid parameters");
- return NULL;
- }
-
- ndays = PyLong_FromLong(_days_in_year(year));
-
- return ndays;
-}
-
-PyObject *timestamp(PyObject *self, PyObject *args)
-{
- int64_t result;
- PyObject *dt;
-
- if (!PyArg_ParseTuple(args, "O", &dt))
- {
- PyErr_SetString(
- PyExc_ValueError, "Invalid parameters");
- return NULL;
- }
-
- int year = (double)PyDateTime_GET_YEAR(dt);
- int month = PyDateTime_GET_MONTH(dt);
- int day = PyDateTime_GET_DAY(dt);
- int hour = PyDateTime_DATE_GET_HOUR(dt);
- int minute = PyDateTime_DATE_GET_MINUTE(dt);
- int second = PyDateTime_DATE_GET_SECOND(dt);
-
- result = (year - 1970) * 365 + MONTHS_OFFSETS[0][month];
- result += (int)floor((double)(year - 1968) / 4);
- result -= (year - 1900) / 100;
- result += (year - 1600) / 400;
-
- if (_is_leap(year) && month < 3)
- {
- result -= 1;
- }
-
- result += day - 1;
- result *= 24;
- result += hour;
- result *= 60;
- result += minute;
- result *= 60;
- result += second;
-
- return PyLong_FromSsize_t(result);
-}
-
-PyObject *local_time(PyObject *self, PyObject *args)
-{
- double unix_time;
- int32_t utc_offset;
- int32_t year;
- int32_t microsecond;
- int64_t seconds;
- int32_t leap_year;
- int64_t sec_per_100years;
- int64_t sec_per_4years;
- int32_t sec_per_year;
- int32_t month;
- int32_t day;
- int32_t month_offset;
- int32_t hour;
- int32_t minute;
- int32_t second;
-
- if (!PyArg_ParseTuple(args, "dii", &unix_time, &utc_offset, &microsecond))
- {
- PyErr_SetString(
- PyExc_ValueError, "Invalid parameters");
- return NULL;
- }
-
- year = EPOCH_YEAR;
- seconds = (int64_t)floor(unix_time);
-
- // Shift to a base year that is 400-year aligned.
- if (seconds >= 0)
- {
- seconds -= 10957L * SECS_PER_DAY;
- year += 30; // == 2000;
- }
- else
- {
- seconds += (int64_t)(146097L - 10957L) * SECS_PER_DAY;
- year -= 370; // == 1600;
- }
-
- seconds += utc_offset;
-
- // Handle years in chunks of 400/100/4/1
- year += 400 * (seconds / SECS_PER_400_YEARS);
- seconds %= SECS_PER_400_YEARS;
- if (seconds < 0)
- {
- seconds += SECS_PER_400_YEARS;
- year -= 400;
- }
-
- leap_year = 1; // 4-century aligned
-
- sec_per_100years = SECS_PER_100_YEARS[leap_year];
-
- while (seconds >= sec_per_100years)
- {
- seconds -= sec_per_100years;
- year += 100;
- leap_year = 0; // 1-century, non 4-century aligned
- sec_per_100years = SECS_PER_100_YEARS[leap_year];
- }
-
- sec_per_4years = SECS_PER_4_YEARS[leap_year];
- while (seconds >= sec_per_4years)
- {
- seconds -= sec_per_4years;
- year += 4;
- leap_year = 1; // 4-year, non century aligned
- sec_per_4years = SECS_PER_4_YEARS[leap_year];
- }
-
- sec_per_year = SECS_PER_YEAR[leap_year];
- while (seconds >= sec_per_year)
- {
- seconds -= sec_per_year;
- year += 1;
- leap_year = 0; // non 4-year aligned
- sec_per_year = SECS_PER_YEAR[leap_year];
- }
-
- // Handle months and days
- month = TM_DECEMBER + 1;
- day = seconds / SECS_PER_DAY + 1;
- seconds %= SECS_PER_DAY;
- while (month != TM_JANUARY + 1)
- {
- month_offset = MONTHS_OFFSETS[leap_year][month];
- if (day > month_offset)
- {
- day -= month_offset;
- break;
- }
-
- month -= 1;
- }
-
- // Handle hours, minutes and seconds
- hour = seconds / SECS_PER_HOUR;
- seconds %= SECS_PER_HOUR;
- minute = seconds / SECS_PER_MIN;
- second = seconds % SECS_PER_MIN;
-
- return Py_BuildValue("NNNNNNN",
- PyLong_FromLong(year),
- PyLong_FromLong(month),
- PyLong_FromLong(day),
- PyLong_FromLong(hour),
- PyLong_FromLong(minute),
- PyLong_FromLong(second),
- PyLong_FromLong(microsecond));
-}
-
-// Calculate a precise difference between two datetimes.
-PyObject *precise_diff(PyObject *self, PyObject *args)
-{
- PyObject *dt1;
- PyObject *dt2;
-
- if (!PyArg_ParseTuple(args, "OO", &dt1, &dt2))
- {
- PyErr_SetString(
- PyExc_ValueError, "Invalid parameters");
- return NULL;
- }
-
- int year_diff = 0;
- int month_diff = 0;
- int day_diff = 0;
- int hour_diff = 0;
- int minute_diff = 0;
- int second_diff = 0;
- int microsecond_diff = 0;
- int sign = 1;
- int year;
- int month;
- int leap;
- int days_in_last_month;
- int days_in_month;
- int dt1_year = PyDateTime_GET_YEAR(dt1);
- int dt2_year = PyDateTime_GET_YEAR(dt2);
- int dt1_month = PyDateTime_GET_MONTH(dt1);
- int dt2_month = PyDateTime_GET_MONTH(dt2);
- int dt1_day = PyDateTime_GET_DAY(dt1);
- int dt2_day = PyDateTime_GET_DAY(dt2);
- int dt1_hour = 0;
- int dt2_hour = 0;
- int dt1_minute = 0;
- int dt2_minute = 0;
- int dt1_second = 0;
- int dt2_second = 0;
- int dt1_microsecond = 0;
- int dt2_microsecond = 0;
- int dt1_total_seconds = 0;
- int dt2_total_seconds = 0;
- int dt1_offset = 0;
- int dt2_offset = 0;
- int dt1_is_datetime = PyDateTime_Check(dt1);
- int dt2_is_datetime = PyDateTime_Check(dt2);
- char *tz1 = "";
- char *tz2 = "";
- int in_same_tz = 0;
- int total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day));
-
- // If both dates are datetimes, we check
- // If we are in the same timezone
- if (dt1_is_datetime && dt2_is_datetime)
- {
- if (_has_tzinfo(dt1))
- {
- tz1 = _get_tz_name(dt1);
- dt1_offset = _get_offset(dt1);
- }
-
- if (_has_tzinfo(dt2))
- {
- tz2 = _get_tz_name(dt2);
- dt2_offset = _get_offset(dt2);
- }
-
- in_same_tz = tz1 == tz2 && strncmp(tz1, "", 1);
- }
-
- // If we have datetimes (and not only dates)
- // we get the information we need
- if (dt1_is_datetime)
- {
- dt1_hour = PyDateTime_DATE_GET_HOUR(dt1);
- dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1);
- dt1_second = PyDateTime_DATE_GET_SECOND(dt1);
- dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1);
-
- if ((!in_same_tz && dt1_offset != 0) || total_days == 0)
- {
- dt1_hour -= dt1_offset / SECS_PER_HOUR;
- dt1_offset %= SECS_PER_HOUR;
- dt1_minute -= dt1_offset / SECS_PER_MIN;
- dt1_offset %= SECS_PER_MIN;
- dt1_second -= dt1_offset;
-
- if (dt1_second < 0)
- {
- dt1_second += 60;
- dt1_minute -= 1;
- }
- else if (dt1_second > 60)
- {
- dt1_second -= 60;
- dt1_minute += 1;
- }
-
- if (dt1_minute < 0)
- {
- dt1_minute += 60;
- dt1_hour -= 1;
- }
- else if (dt1_minute > 60)
- {
- dt1_minute -= 60;
- dt1_hour += 1;
- }
-
- if (dt1_hour < 0)
- {
- dt1_hour += 24;
- dt1_day -= 1;
- }
- else if (dt1_hour > 24)
- {
- dt1_hour -= 24;
- dt1_day += 1;
- }
- }
-
- dt1_total_seconds = (dt1_hour * SECS_PER_HOUR + dt1_minute * SECS_PER_MIN + dt1_second);
- }
-
- if (dt2_is_datetime)
- {
- dt2_hour = PyDateTime_DATE_GET_HOUR(dt2);
- dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2);
- dt2_second = PyDateTime_DATE_GET_SECOND(dt2);
- dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2);
-
- if ((!in_same_tz && dt2_offset != 0) || total_days == 0)
- {
- dt2_hour -= dt2_offset / SECS_PER_HOUR;
- dt2_offset %= SECS_PER_HOUR;
- dt2_minute -= dt2_offset / SECS_PER_MIN;
- dt2_offset %= SECS_PER_MIN;
- dt2_second -= dt2_offset;
-
- if (dt2_second < 0)
- {
- dt2_second += 60;
- dt2_minute -= 1;
- }
- else if (dt2_second > 60)
- {
- dt2_second -= 60;
- dt2_minute += 1;
- }
-
- if (dt2_minute < 0)
- {
- dt2_minute += 60;
- dt2_hour -= 1;
- }
- else if (dt2_minute > 60)
- {
- dt2_minute -= 60;
- dt2_hour += 1;
- }
-
- if (dt2_hour < 0)
- {
- dt2_hour += 24;
- dt2_day -= 1;
- }
- else if (dt2_hour > 24)
- {
- dt2_hour -= 24;
- dt2_day += 1;
- }
- }
-
- dt2_total_seconds = (dt2_hour * SECS_PER_HOUR + dt2_minute * SECS_PER_MIN + dt2_second);
- }
-
- // Direct comparison between two datetimes does not work
- // so we need to check by properties
- int dt1_gt_dt2 = (dt1_year > dt2_year || (dt1_year == dt2_year && dt1_month > dt2_month) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day > dt2_day) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds > dt2_total_seconds) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds == dt2_total_seconds && dt1_microsecond > dt2_microsecond));
-
- if (dt1_gt_dt2)
- {
- PyObject *temp;
- temp = dt1;
- dt1 = dt2;
- dt2 = temp;
- sign = -1;
-
- // Retrieving properties
- dt1_year = PyDateTime_GET_YEAR(dt1);
- dt2_year = PyDateTime_GET_YEAR(dt2);
- dt1_month = PyDateTime_GET_MONTH(dt1);
- dt2_month = PyDateTime_GET_MONTH(dt2);
- dt1_day = PyDateTime_GET_DAY(dt1);
- dt2_day = PyDateTime_GET_DAY(dt2);
-
- if (dt2_is_datetime)
- {
- dt1_hour = PyDateTime_DATE_GET_HOUR(dt1);
- dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1);
- dt1_second = PyDateTime_DATE_GET_SECOND(dt1);
- dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1);
- }
-
- if (dt1_is_datetime)
- {
- dt2_hour = PyDateTime_DATE_GET_HOUR(dt2);
- dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2);
- dt2_second = PyDateTime_DATE_GET_SECOND(dt2);
- dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2);
- }
-
- total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day));
- }
-
- year_diff = dt2_year - dt1_year;
- month_diff = dt2_month - dt1_month;
- day_diff = dt2_day - dt1_day;
- hour_diff = dt2_hour - dt1_hour;
- minute_diff = dt2_minute - dt1_minute;
- second_diff = dt2_second - dt1_second;
- microsecond_diff = dt2_microsecond - dt1_microsecond;
-
- if (microsecond_diff < 0)
- {
- microsecond_diff += 1e6;
- second_diff -= 1;
- }
-
- if (second_diff < 0)
- {
- second_diff += 60;
- minute_diff -= 1;
- }
-
- if (minute_diff < 0)
- {
- minute_diff += 60;
- hour_diff -= 1;
- }
-
- if (hour_diff < 0)
- {
- hour_diff += 24;
- day_diff -= 1;
- }
-
- if (day_diff < 0)
- {
- // If we have a difference in days,
- // we have to check if they represent months
- year = dt2_year;
- month = dt2_month;
-
- if (month == 1)
- {
- month = 12;
- year -= 1;
- }
- else
- {
- month -= 1;
- }
-
- leap = _is_leap(year);
-
- days_in_last_month = DAYS_PER_MONTHS[leap][month];
- days_in_month = DAYS_PER_MONTHS[_is_leap(dt2_year)][dt2_month];
-
- if (day_diff < days_in_month - days_in_last_month)
- {
- // We don't have a full month, we calculate days
- if (days_in_last_month < dt1_day)
- {
- day_diff += dt1_day;
- }
- else
- {
- day_diff += days_in_last_month;
- }
- }
- else if (day_diff == days_in_month - days_in_last_month)
- {
- // We have exactly a full month
- // We remove the days difference
- // and add one to the months difference
- day_diff = 0;
- month_diff += 1;
- }
- else
- {
- // We have a full month
- day_diff += days_in_last_month;
- }
-
- month_diff -= 1;
- }
-
- if (month_diff < 0)
- {
- month_diff += 12;
- year_diff -= 1;
- }
-
- return new_diff(
- year_diff * sign,
- month_diff * sign,
- day_diff * sign,
- hour_diff * sign,
- minute_diff * sign,
- second_diff * sign,
- microsecond_diff * sign,
- total_days * sign);
-}
-
-/* ------------------------------------------------------------------------- */
-
-static PyMethodDef helpers_methods[] = {
- {"is_leap",
- (PyCFunction)is_leap,
- METH_VARARGS,
- PyDoc_STR("Checks if a year is a leap year.")},
- {"is_long_year",
- (PyCFunction)is_long_year,
- METH_VARARGS,
- PyDoc_STR("Checks if a year is a long year.")},
- {"week_day",
- (PyCFunction)week_day,
- METH_VARARGS,
- PyDoc_STR("Returns the weekday number.")},
- {"days_in_year",
- (PyCFunction)days_in_year,
- METH_VARARGS,
- PyDoc_STR("Returns the number of days in the given year.")},
- {"timestamp",
- (PyCFunction)timestamp,
- METH_VARARGS,
- PyDoc_STR("Returns the timestamp of the given datetime.")},
- {"local_time",
- (PyCFunction)local_time,
- METH_VARARGS,
- PyDoc_STR("Returns a UNIX time as a broken down time for a particular transition type.")},
- {"precise_diff",
- (PyCFunction)precise_diff,
- METH_VARARGS,
- PyDoc_STR("Calculate a precise difference between two datetimes.")},
- {NULL}};
-
-/* ------------------------------------------------------------------------- */
-
-static struct PyModuleDef moduledef = {
- PyModuleDef_HEAD_INIT,
- "_helpers",
- NULL,
- -1,
- helpers_methods,
- NULL,
- NULL,
- NULL,
- NULL,
-};
-
-PyMODINIT_FUNC
-PyInit__helpers(void)
-{
- PyObject *module;
-
- PyDateTime_IMPORT;
-
- module = PyModule_Create(&moduledef);
-
- if (module == NULL)
- return NULL;
-
- // Diff declaration
- Diff_type.tp_new = PyType_GenericNew;
- Diff_type.tp_members = Diff_members;
- Diff_type.tp_init = (initproc)Diff_init;
-
- if (PyType_Ready(&Diff_type) < 0)
- return NULL;
-
- PyModule_AddObject(module, "PreciseDiff", (PyObject *)&Diff_type);
-
- return module;
-}
+/* ------------------------------------------------------------------------- */
+
+#include <Python.h>
+#include <datetime.h>
+#include <structmember.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+/* ------------------------------------------------------------------------- */
+
+#define EPOCH_YEAR 1970
+
+#define DAYS_PER_N_YEAR 365
+#define DAYS_PER_L_YEAR 366
+
+#define USECS_PER_SEC 1000000
+
+#define SECS_PER_MIN 60
+#define SECS_PER_HOUR (60 * SECS_PER_MIN)
+#define SECS_PER_DAY (SECS_PER_HOUR * 24)
+
+// 400-year chunks always have 146097 days (20871 weeks).
+#define DAYS_PER_400_YEARS 146097L
+#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)SECS_PER_DAY)
+
+// The number of seconds in an aligned 100-year chunk, for those that
+// do not begin with a leap year and those that do respectively.
+const int64_t SECS_PER_100_YEARS[2] = {
+ (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY,
+ (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * DAYS_PER_L_YEAR) * SECS_PER_DAY};
+
+// The number of seconds in an aligned 4-year chunk, for those that
+// do not begin with a leap year and those that do respectively.
+const int32_t SECS_PER_4_YEARS[2] = {
+ (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY,
+ (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY};
+
+// The number of seconds in non-leap and leap years respectively.
+const int32_t SECS_PER_YEAR[2] = {
+ DAYS_PER_N_YEAR * SECS_PER_DAY,
+ DAYS_PER_L_YEAR *SECS_PER_DAY};
+
+#define MONTHS_PER_YEAR 12
+
+// The month lengths in non-leap and leap years respectively.
+const int32_t DAYS_PER_MONTHS[2][13] = {
+ {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
+ {-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};
+
+// The day offsets of the beginning of each (1-based) month in non-leap
+// and leap years respectively.
+// For example, in a leap year there are 335 days before December.
+const int32_t MONTHS_OFFSETS[2][14] = {
+ {-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365},
+ {-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}};
+
+const int DAY_OF_WEEK_TABLE[12] = {
+ 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
+
+#define TM_SUNDAY 0
+#define TM_MONDAY 1
+#define TM_TUESDAY 2
+#define TM_WEDNESDAY 3
+#define TM_THURSDAY 4
+#define TM_FRIDAY 5
+#define TM_SATURDAY 6
+
+#define TM_JANUARY 0
+#define TM_FEBRUARY 1
+#define TM_MARCH 2
+#define TM_APRIL 3
+#define TM_MAY 4
+#define TM_JUNE 5
+#define TM_JULY 6
+#define TM_AUGUST 7
+#define TM_SEPTEMBER 8
+#define TM_OCTOBER 9
+#define TM_NOVEMBER 10
+#define TM_DECEMBER 11
+
+/* ------------------------------------------------------------------------- */
+
+int _p(int y)
+{
+ return y + y / 4 - y / 100 + y / 400;
+}
+
+int _is_leap(int year)
+{
+ return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
+}
+
+int _is_long_year(int year)
+{
+ return (_p(year) % 7 == 4) || (_p(year - 1) % 7 == 3);
+}
+
+int _week_day(int year, int month, int day)
+{
+ int y;
+ int w;
+
+ y = year - (month < 3);
+
+ w = (_p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7;
+
+ if (!w)
+ {
+ w = 7;
+ }
+
+ return w;
+}
+
+int _days_in_year(int year)
+{
+ if (_is_leap(year))
+ {
+ return DAYS_PER_L_YEAR;
+ }
+
+ return DAYS_PER_N_YEAR;
+}
+
+int _day_number(int year, int month, int day)
+{
+ month = (month + 9) % 12;
+ year = year - month / 10;
+
+ return (
+ 365 * year + year / 4 - year / 100 + year / 400 + (month * 306 + 5) / 10 + (day - 1));
+}
+
+int _get_offset(PyObject *dt)
+{
+ PyObject *tzinfo;
+ PyObject *offset;
+
+ tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo;
+
+ if (tzinfo != Py_None)
+ {
+ offset = PyObject_CallMethod(tzinfo, "utcoffset", "O", dt);
+
+ return PyDateTime_DELTA_GET_DAYS(offset) * SECS_PER_DAY + PyDateTime_DELTA_GET_SECONDS(offset);
+ }
+
+ return 0;
+}
+
+int _has_tzinfo(PyObject *dt)
+{
+ return ((_PyDateTime_BaseTZInfo *)(dt))->hastzinfo;
+}
+
+char *_get_tz_name(PyObject *dt)
+{
+ PyObject *tzinfo;
+ char *tz = "";
+
+ tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo;
+
+ if (tzinfo != Py_None)
+ {
+ if (PyObject_HasAttrString(tzinfo, "key"))
+ {
+ // zoneinfo timezone
+ tz = (char *)PyUnicode_AsUTF8(
+ PyObject_GetAttrString(tzinfo, "name"));
+ }
+ else if (PyObject_HasAttrString(tzinfo, "name"))
+ {
+ // Pendulum timezone
+ tz = (char *)PyUnicode_AsUTF8(
+ PyObject_GetAttrString(tzinfo, "name"));
+ }
+ else if (PyObject_HasAttrString(tzinfo, "zone"))
+ {
+ // pytz timezone
+ tz = (char *)PyUnicode_AsUTF8(
+ PyObject_GetAttrString(tzinfo, "zone"));
+ }
+ }
+
+ return tz;
+}
+
+/* ------------------------ Custom Types ------------------------------- */
+
+/*
+ * class Diff():
+ */
+typedef struct
+{
+ PyObject_HEAD int years;
+ int months;
+ int days;
+ int hours;
+ int minutes;
+ int seconds;
+ int microseconds;
+ int total_days;
+} Diff;
+
+/*
+ * def __init__(self, years, months, days, hours, minutes, seconds, microseconds, total_days):
+ * self.years = years
+ * # ...
+*/
+static int Diff_init(Diff *self, PyObject *args, PyObject *kwargs)
+{
+ int years;
+ int months;
+ int days;
+ int hours;
+ int minutes;
+ int seconds;
+ int microseconds;
+ int total_days;
+
+ if (!PyArg_ParseTuple(args, "iiiiiii", &years, &months, &days, &hours, &minutes, &seconds, &microseconds, &total_days))
+ return -1;
+
+ self->years = years;
+ self->months = months;
+ self->days = days;
+ self->hours = hours;
+ self->minutes = minutes;
+ self->seconds = seconds;
+ self->microseconds = microseconds;
+ self->total_days = total_days;
+
+ return 0;
+}
+
+/*
+ * def __repr__(self):
+ * return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format(
+ * self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds
+ * )
+ */
+static PyObject *Diff_repr(Diff *self)
+{
+ return PyUnicode_FromFormat(
+ "%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);
+}
+
+/*
+ * Instantiate new Diff_type object
+ * Skip overhead of calling PyObject_New and PyObject_Init.
+ * Directly allocate object.
+ */
+static PyObject *new_diff_ex(int years, int months, int days, int hours, int minutes, int seconds, int microseconds, int total_days, PyTypeObject *type)
+{
+ Diff *self = (Diff *)(type->tp_alloc(type, 0));
+
+ if (self != NULL)
+ {
+ self->years = years;
+ self->months = months;
+ self->days = days;
+ self->hours = hours;
+ self->minutes = minutes;
+ self->seconds = seconds;
+ self->microseconds = microseconds;
+ self->total_days = total_days;
+ }
+
+ return (PyObject *)self;
+}
+
+/*
+ * Class member / class attributes
+ */
+static PyMemberDef Diff_members[] = {
+ {"years", T_INT, offsetof(Diff, years), 0, "years in diff"},
+ {"months", T_INT, offsetof(Diff, months), 0, "months in diff"},
+ {"days", T_INT, offsetof(Diff, days), 0, "days in diff"},
+ {"hours", T_INT, offsetof(Diff, hours), 0, "hours in diff"},
+ {"minutes", T_INT, offsetof(Diff, minutes), 0, "minutes in diff"},
+ {"seconds", T_INT, offsetof(Diff, seconds), 0, "seconds in diff"},
+ {"microseconds", T_INT, offsetof(Diff, microseconds), 0, "microseconds in diff"},
+ {"total_days", T_INT, offsetof(Diff, total_days), 0, "total days in diff"},
+ {NULL}};
+
+static PyTypeObject Diff_type = {
+ PyVarObject_HEAD_INIT(NULL, 0) "PreciseDiff", /* tp_name */
+ sizeof(Diff), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ 0, /* tp_dealloc */
+ 0, /* tp_print */
+ 0, /* tp_getattr */
+ 0, /* tp_setattr */
+ 0, /* tp_as_async */
+ (reprfunc)Diff_repr, /* tp_repr */
+ 0, /* tp_as_number */
+ 0, /* tp_as_sequence */
+ 0, /* tp_as_mapping */
+ 0, /* tp_hash */
+ 0, /* tp_call */
+ (reprfunc)Diff_repr, /* tp_str */
+ 0, /* tp_getattro */
+ 0, /* tp_setattro */
+ 0, /* tp_as_buffer */
+ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
+ "Precise difference between two datetime objects", /* tp_doc */
+};
+
+#define new_diff(years, months, days, hours, minutes, seconds, microseconds, total_days) new_diff_ex(years, months, days, hours, minutes, seconds, microseconds, total_days, &Diff_type)
+
+/* -------------------------- Functions --------------------------*/
+
+PyObject *is_leap(PyObject *self, PyObject *args)
+{
+ PyObject *leap;
+ int year;
+
+ if (!PyArg_ParseTuple(args, "i", &year))
+ {
+ PyErr_SetString(
+ PyExc_ValueError, "Invalid parameters");
+ return NULL;
+ }
+
+ leap = PyBool_FromLong(_is_leap(year));
+
+ return leap;
+}
+
+PyObject *is_long_year(PyObject *self, PyObject *args)
+{
+ PyObject *is_long;
+ int year;
+
+ if (!PyArg_ParseTuple(args, "i", &year))
+ {
+ PyErr_SetString(
+ PyExc_ValueError, "Invalid parameters");
+ return NULL;
+ }
+
+ is_long = PyBool_FromLong(_is_long_year(year));
+
+ return is_long;
+}
+
+PyObject *week_day(PyObject *self, PyObject *args)
+{
+ PyObject *wd;
+ int year;
+ int month;
+ int day;
+
+ if (!PyArg_ParseTuple(args, "iii", &year, &month, &day))
+ {
+ PyErr_SetString(
+ PyExc_ValueError, "Invalid parameters");
+ return NULL;
+ }
+
+ wd = PyLong_FromLong(_week_day(year, month, day));
+
+ return wd;
+}
+
+PyObject *days_in_year(PyObject *self, PyObject *args)
+{
+ PyObject *ndays;
+ int year;
+
+ if (!PyArg_ParseTuple(args, "i", &year))
+ {
+ PyErr_SetString(
+ PyExc_ValueError, "Invalid parameters");
+ return NULL;
+ }
+
+ ndays = PyLong_FromLong(_days_in_year(year));
+
+ return ndays;
+}
+
+PyObject *timestamp(PyObject *self, PyObject *args)
+{
+ int64_t result;
+ PyObject *dt;
+
+ if (!PyArg_ParseTuple(args, "O", &dt))
+ {
+ PyErr_SetString(
+ PyExc_ValueError, "Invalid parameters");
+ return NULL;
+ }
+
+ int year = (double)PyDateTime_GET_YEAR(dt);
+ int month = PyDateTime_GET_MONTH(dt);
+ int day = PyDateTime_GET_DAY(dt);
+ int hour = PyDateTime_DATE_GET_HOUR(dt);
+ int minute = PyDateTime_DATE_GET_MINUTE(dt);
+ int second = PyDateTime_DATE_GET_SECOND(dt);
+
+ result = (year - 1970) * 365 + MONTHS_OFFSETS[0][month];
+ result += (int)floor((double)(year - 1968) / 4);
+ result -= (year - 1900) / 100;
+ result += (year - 1600) / 400;
+
+ if (_is_leap(year) && month < 3)
+ {
+ result -= 1;
+ }
+
+ result += day - 1;
+ result *= 24;
+ result += hour;
+ result *= 60;
+ result += minute;
+ result *= 60;
+ result += second;
+
+ return PyLong_FromSsize_t(result);
+}
+
+PyObject *local_time(PyObject *self, PyObject *args)
+{
+ double unix_time;
+ int32_t utc_offset;
+ int32_t year;
+ int32_t microsecond;
+ int64_t seconds;
+ int32_t leap_year;
+ int64_t sec_per_100years;
+ int64_t sec_per_4years;
+ int32_t sec_per_year;
+ int32_t month;
+ int32_t day;
+ int32_t month_offset;
+ int32_t hour;
+ int32_t minute;
+ int32_t second;
+
+ if (!PyArg_ParseTuple(args, "dii", &unix_time, &utc_offset, &microsecond))
+ {
+ PyErr_SetString(
+ PyExc_ValueError, "Invalid parameters");
+ return NULL;
+ }
+
+ year = EPOCH_YEAR;
+ seconds = (int64_t)floor(unix_time);
+
+ // Shift to a base year that is 400-year aligned.
+ if (seconds >= 0)
+ {
+ seconds -= 10957L * SECS_PER_DAY;
+ year += 30; // == 2000;
+ }
+ else
+ {
+ seconds += (int64_t)(146097L - 10957L) * SECS_PER_DAY;
+ year -= 370; // == 1600;
+ }
+
+ seconds += utc_offset;
+
+ // Handle years in chunks of 400/100/4/1
+ year += 400 * (seconds / SECS_PER_400_YEARS);
+ seconds %= SECS_PER_400_YEARS;
+ if (seconds < 0)
+ {
+ seconds += SECS_PER_400_YEARS;
+ year -= 400;
+ }
+
+ leap_year = 1; // 4-century aligned
+
+ sec_per_100years = SECS_PER_100_YEARS[leap_year];
+
+ while (seconds >= sec_per_100years)
+ {
+ seconds -= sec_per_100years;
+ year += 100;
+ leap_year = 0; // 1-century, non 4-century aligned
+ sec_per_100years = SECS_PER_100_YEARS[leap_year];
+ }
+
+ sec_per_4years = SECS_PER_4_YEARS[leap_year];
+ while (seconds >= sec_per_4years)
+ {
+ seconds -= sec_per_4years;
+ year += 4;
+ leap_year = 1; // 4-year, non century aligned
+ sec_per_4years = SECS_PER_4_YEARS[leap_year];
+ }
+
+ sec_per_year = SECS_PER_YEAR[leap_year];
+ while (seconds >= sec_per_year)
+ {
+ seconds -= sec_per_year;
+ year += 1;
+ leap_year = 0; // non 4-year aligned
+ sec_per_year = SECS_PER_YEAR[leap_year];
+ }
+
+ // Handle months and days
+ month = TM_DECEMBER + 1;
+ day = seconds / SECS_PER_DAY + 1;
+ seconds %= SECS_PER_DAY;
+ while (month != TM_JANUARY + 1)
+ {
+ month_offset = MONTHS_OFFSETS[leap_year][month];
+ if (day > month_offset)
+ {
+ day -= month_offset;
+ break;
+ }
+
+ month -= 1;
+ }
+
+ // Handle hours, minutes and seconds
+ hour = seconds / SECS_PER_HOUR;
+ seconds %= SECS_PER_HOUR;
+ minute = seconds / SECS_PER_MIN;
+ second = seconds % SECS_PER_MIN;
+
+ return Py_BuildValue("NNNNNNN",
+ PyLong_FromLong(year),
+ PyLong_FromLong(month),
+ PyLong_FromLong(day),
+ PyLong_FromLong(hour),
+ PyLong_FromLong(minute),
+ PyLong_FromLong(second),
+ PyLong_FromLong(microsecond));
+}
+
+// Calculate a precise difference between two datetimes.
+PyObject *precise_diff(PyObject *self, PyObject *args)
+{
+ PyObject *dt1;
+ PyObject *dt2;
+
+ if (!PyArg_ParseTuple(args, "OO", &dt1, &dt2))
+ {
+ PyErr_SetString(
+ PyExc_ValueError, "Invalid parameters");
+ return NULL;
+ }
+
+ int year_diff = 0;
+ int month_diff = 0;
+ int day_diff = 0;
+ int hour_diff = 0;
+ int minute_diff = 0;
+ int second_diff = 0;
+ int microsecond_diff = 0;
+ int sign = 1;
+ int year;
+ int month;
+ int leap;
+ int days_in_last_month;
+ int days_in_month;
+ int dt1_year = PyDateTime_GET_YEAR(dt1);
+ int dt2_year = PyDateTime_GET_YEAR(dt2);
+ int dt1_month = PyDateTime_GET_MONTH(dt1);
+ int dt2_month = PyDateTime_GET_MONTH(dt2);
+ int dt1_day = PyDateTime_GET_DAY(dt1);
+ int dt2_day = PyDateTime_GET_DAY(dt2);
+ int dt1_hour = 0;
+ int dt2_hour = 0;
+ int dt1_minute = 0;
+ int dt2_minute = 0;
+ int dt1_second = 0;
+ int dt2_second = 0;
+ int dt1_microsecond = 0;
+ int dt2_microsecond = 0;
+ int dt1_total_seconds = 0;
+ int dt2_total_seconds = 0;
+ int dt1_offset = 0;
+ int dt2_offset = 0;
+ int dt1_is_datetime = PyDateTime_Check(dt1);
+ int dt2_is_datetime = PyDateTime_Check(dt2);
+ char *tz1 = "";
+ char *tz2 = "";
+ int in_same_tz = 0;
+ int total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day));
+
+ // If both dates are datetimes, we check
+ // If we are in the same timezone
+ if (dt1_is_datetime && dt2_is_datetime)
+ {
+ if (_has_tzinfo(dt1))
+ {
+ tz1 = _get_tz_name(dt1);
+ dt1_offset = _get_offset(dt1);
+ }
+
+ if (_has_tzinfo(dt2))
+ {
+ tz2 = _get_tz_name(dt2);
+ dt2_offset = _get_offset(dt2);
+ }
+
+ in_same_tz = tz1 == tz2 && strncmp(tz1, "", 1);
+ }
+
+ // If we have datetimes (and not only dates)
+ // we get the information we need
+ if (dt1_is_datetime)
+ {
+ dt1_hour = PyDateTime_DATE_GET_HOUR(dt1);
+ dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1);
+ dt1_second = PyDateTime_DATE_GET_SECOND(dt1);
+ dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1);
+
+ if ((!in_same_tz && dt1_offset != 0) || total_days == 0)
+ {
+ dt1_hour -= dt1_offset / SECS_PER_HOUR;
+ dt1_offset %= SECS_PER_HOUR;
+ dt1_minute -= dt1_offset / SECS_PER_MIN;
+ dt1_offset %= SECS_PER_MIN;
+ dt1_second -= dt1_offset;
+
+ if (dt1_second < 0)
+ {
+ dt1_second += 60;
+ dt1_minute -= 1;
+ }
+ else if (dt1_second > 60)
+ {
+ dt1_second -= 60;
+ dt1_minute += 1;
+ }
+
+ if (dt1_minute < 0)
+ {
+ dt1_minute += 60;
+ dt1_hour -= 1;
+ }
+ else if (dt1_minute > 60)
+ {
+ dt1_minute -= 60;
+ dt1_hour += 1;
+ }
+
+ if (dt1_hour < 0)
+ {
+ dt1_hour += 24;
+ dt1_day -= 1;
+ }
+ else if (dt1_hour > 24)
+ {
+ dt1_hour -= 24;
+ dt1_day += 1;
+ }
+ }
+
+ dt1_total_seconds = (dt1_hour * SECS_PER_HOUR + dt1_minute * SECS_PER_MIN + dt1_second);
+ }
+
+ if (dt2_is_datetime)
+ {
+ dt2_hour = PyDateTime_DATE_GET_HOUR(dt2);
+ dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2);
+ dt2_second = PyDateTime_DATE_GET_SECOND(dt2);
+ dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2);
+
+ if ((!in_same_tz && dt2_offset != 0) || total_days == 0)
+ {
+ dt2_hour -= dt2_offset / SECS_PER_HOUR;
+ dt2_offset %= SECS_PER_HOUR;
+ dt2_minute -= dt2_offset / SECS_PER_MIN;
+ dt2_offset %= SECS_PER_MIN;
+ dt2_second -= dt2_offset;
+
+ if (dt2_second < 0)
+ {
+ dt2_second += 60;
+ dt2_minute -= 1;
+ }
+ else if (dt2_second > 60)
+ {
+ dt2_second -= 60;
+ dt2_minute += 1;
+ }
+
+ if (dt2_minute < 0)
+ {
+ dt2_minute += 60;
+ dt2_hour -= 1;
+ }
+ else if (dt2_minute > 60)
+ {
+ dt2_minute -= 60;
+ dt2_hour += 1;
+ }
+
+ if (dt2_hour < 0)
+ {
+ dt2_hour += 24;
+ dt2_day -= 1;
+ }
+ else if (dt2_hour > 24)
+ {
+ dt2_hour -= 24;
+ dt2_day += 1;
+ }
+ }
+
+ dt2_total_seconds = (dt2_hour * SECS_PER_HOUR + dt2_minute * SECS_PER_MIN + dt2_second);
+ }
+
+ // Direct comparison between two datetimes does not work
+ // so we need to check by properties
+ int dt1_gt_dt2 = (dt1_year > dt2_year || (dt1_year == dt2_year && dt1_month > dt2_month) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day > dt2_day) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds > dt2_total_seconds) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds == dt2_total_seconds && dt1_microsecond > dt2_microsecond));
+
+ if (dt1_gt_dt2)
+ {
+ PyObject *temp;
+ temp = dt1;
+ dt1 = dt2;
+ dt2 = temp;
+ sign = -1;
+
+ // Retrieving properties
+ dt1_year = PyDateTime_GET_YEAR(dt1);
+ dt2_year = PyDateTime_GET_YEAR(dt2);
+ dt1_month = PyDateTime_GET_MONTH(dt1);
+ dt2_month = PyDateTime_GET_MONTH(dt2);
+ dt1_day = PyDateTime_GET_DAY(dt1);
+ dt2_day = PyDateTime_GET_DAY(dt2);
+
+ if (dt2_is_datetime)
+ {
+ dt1_hour = PyDateTime_DATE_GET_HOUR(dt1);
+ dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1);
+ dt1_second = PyDateTime_DATE_GET_SECOND(dt1);
+ dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1);
+ }
+
+ if (dt1_is_datetime)
+ {
+ dt2_hour = PyDateTime_DATE_GET_HOUR(dt2);
+ dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2);
+ dt2_second = PyDateTime_DATE_GET_SECOND(dt2);
+ dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2);
+ }
+
+ total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day));
+ }
+
+ year_diff = dt2_year - dt1_year;
+ month_diff = dt2_month - dt1_month;
+ day_diff = dt2_day - dt1_day;
+ hour_diff = dt2_hour - dt1_hour;
+ minute_diff = dt2_minute - dt1_minute;
+ second_diff = dt2_second - dt1_second;
+ microsecond_diff = dt2_microsecond - dt1_microsecond;
+
+ if (microsecond_diff < 0)
+ {
+ microsecond_diff += 1e6;
+ second_diff -= 1;
+ }
+
+ if (second_diff < 0)
+ {
+ second_diff += 60;
+ minute_diff -= 1;
+ }
+
+ if (minute_diff < 0)
+ {
+ minute_diff += 60;
+ hour_diff -= 1;
+ }
+
+ if (hour_diff < 0)
+ {
+ hour_diff += 24;
+ day_diff -= 1;
+ }
+
+ if (day_diff < 0)
+ {
+ // If we have a difference in days,
+ // we have to check if they represent months
+ year = dt2_year;
+ month = dt2_month;
+
+ if (month == 1)
+ {
+ month = 12;
+ year -= 1;
+ }
+ else
+ {
+ month -= 1;
+ }
+
+ leap = _is_leap(year);
+
+ days_in_last_month = DAYS_PER_MONTHS[leap][month];
+ days_in_month = DAYS_PER_MONTHS[_is_leap(dt2_year)][dt2_month];
+
+ if (day_diff < days_in_month - days_in_last_month)
+ {
+ // We don't have a full month, we calculate days
+ if (days_in_last_month < dt1_day)
+ {
+ day_diff += dt1_day;
+ }
+ else
+ {
+ day_diff += days_in_last_month;
+ }
+ }
+ else if (day_diff == days_in_month - days_in_last_month)
+ {
+ // We have exactly a full month
+ // We remove the days difference
+ // and add one to the months difference
+ day_diff = 0;
+ month_diff += 1;
+ }
+ else
+ {
+ // We have a full month
+ day_diff += days_in_last_month;
+ }
+
+ month_diff -= 1;
+ }
+
+ if (month_diff < 0)
+ {
+ month_diff += 12;
+ year_diff -= 1;
+ }
+
+ return new_diff(
+ year_diff * sign,
+ month_diff * sign,
+ day_diff * sign,
+ hour_diff * sign,
+ minute_diff * sign,
+ second_diff * sign,
+ microsecond_diff * sign,
+ total_days * sign);
+}
+
+/* ------------------------------------------------------------------------- */
+
+static PyMethodDef helpers_methods[] = {
+ {"is_leap",
+ (PyCFunction)is_leap,
+ METH_VARARGS,
+ PyDoc_STR("Checks if a year is a leap year.")},
+ {"is_long_year",
+ (PyCFunction)is_long_year,
+ METH_VARARGS,
+ PyDoc_STR("Checks if a year is a long year.")},
+ {"week_day",
+ (PyCFunction)week_day,
+ METH_VARARGS,
+ PyDoc_STR("Returns the weekday number.")},
+ {"days_in_year",
+ (PyCFunction)days_in_year,
+ METH_VARARGS,
+ PyDoc_STR("Returns the number of days in the given year.")},
+ {"timestamp",
+ (PyCFunction)timestamp,
+ METH_VARARGS,
+ PyDoc_STR("Returns the timestamp of the given datetime.")},
+ {"local_time",
+ (PyCFunction)local_time,
+ METH_VARARGS,
+ PyDoc_STR("Returns a UNIX time as a broken down time for a particular transition type.")},
+ {"precise_diff",
+ (PyCFunction)precise_diff,
+ METH_VARARGS,
+ PyDoc_STR("Calculate a precise difference between two datetimes.")},
+ {NULL}};
+
+/* ------------------------------------------------------------------------- */
+
+static struct PyModuleDef moduledef = {
+ PyModuleDef_HEAD_INIT,
+ "_helpers",
+ NULL,
+ -1,
+ helpers_methods,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+};
+
+PyMODINIT_FUNC
+PyInit__helpers(void)
+{
+ PyObject *module;
+
+ PyDateTime_IMPORT;
+
+ module = PyModule_Create(&moduledef);
+
+ if (module == NULL)
+ return NULL;
+
+ // Diff declaration
+ Diff_type.tp_new = PyType_GenericNew;
+ Diff_type.tp_members = Diff_members;
+ Diff_type.tp_init = (initproc)Diff_init;
+
+ if (PyType_Ready(&Diff_type) < 0)
+ return NULL;
+
+ PyModule_AddObject(module, "PreciseDiff", (PyObject *)&Diff_type);
+
+ return module;
+}
diff --git a/pendulum/_extensions/_helpers.pyi b/pendulum/_extensions/_helpers.pyi
new file mode 100644
index 0000000..99a5397
--- /dev/null
+++ b/pendulum/_extensions/_helpers.pyi
@@ -0,0 +1,31 @@
+from __future__ import annotations
+
+from collections import namedtuple
+from datetime import date
+from datetime import datetime
+
+def days_in_year(year: int) -> int: ...
+def is_leap(year: int) -> bool: ...
+def is_long_year(year: int) -> bool: ...
+def local_time(
+ unix_time: int, utc_offset: int, microseconds: int
+) -> tuple[int, int, int, int, int, int, int]: ...
+
+class PreciseDiff(
+ namedtuple(
+ "PreciseDiff",
+ "years months days " "hours minutes seconds microseconds " "total_days",
+ )
+):
+ years: int
+ months: int
+ days: int
+ hours: int
+ minutes: int
+ seconds: int
+ microseconds: int
+ total_days: int
+
+def precise_diff(d1: datetime | date, d2: datetime | date) -> PreciseDiff: ...
+def timestamp(dt: datetime) -> int: ...
+def week_day(year: int, month: int, day: int) -> int: ...
diff --git a/pendulum/_extensions/helpers.py b/pendulum/_extensions/helpers.py
index 16d078c..01066a3 100644
--- a/pendulum/_extensions/helpers.py
+++ b/pendulum/_extensions/helpers.py
@@ -1,358 +1,364 @@
-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)
- )
+from __future__ import annotations
+
+import datetime
+import math
+
+from collections import namedtuple
+from typing import cast
+
+from pendulum.constants import DAY_OF_WEEK_TABLE
+from pendulum.constants import DAYS_PER_L_YEAR
+from pendulum.constants import DAYS_PER_MONTHS
+from pendulum.constants import DAYS_PER_N_YEAR
+from pendulum.constants import EPOCH_YEAR
+from pendulum.constants import MONTHS_OFFSETS
+from pendulum.constants import SECS_PER_4_YEARS
+from pendulum.constants import SECS_PER_100_YEARS
+from pendulum.constants import SECS_PER_400_YEARS
+from pendulum.constants import SECS_PER_DAY
+from pendulum.constants import SECS_PER_HOUR
+from pendulum.constants import SECS_PER_MIN
+from pendulum.constants import SECS_PER_YEAR
+from pendulum.constants import TM_DECEMBER
+from pendulum.constants import TM_JANUARY
+from pendulum.tz.timezone import Timezone
+from pendulum.utils._compat import zoneinfo
+
+
+class PreciseDiff(
+ namedtuple(
+ "PreciseDiff",
+ "years months days " "hours minutes seconds microseconds " "total_days",
+ )
+):
+ years: int
+ months: int
+ days: int
+ hours: int
+ minutes: int
+ seconds: int
+ microseconds: int
+ total_days: int
+
+ def __repr__(self) -> str:
+ return (
+ f"{self.years} years "
+ f"{self.months} months "
+ f"{self.days} days "
+ f"{self.hours} hours "
+ f"{self.minutes} minutes "
+ f"{self.seconds} seconds "
+ f"{self.microseconds} microseconds"
+ )
+
+
+def is_leap(year: int) -> bool:
+ return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
+
+
+def is_long_year(year: int) -> bool:
+ def p(y: int) -> int:
+ return y + y // 4 - y // 100 + y // 400
+
+ return p(year) % 7 == 4 or p(year - 1) % 7 == 3
+
+
+def week_day(year: int, month: int, day: int) -> int:
+ if month < 3:
+ year -= 1
+
+ w = (
+ year
+ + year // 4
+ - year // 100
+ + year // 400
+ + DAY_OF_WEEK_TABLE[month - 1]
+ + day
+ ) % 7
+
+ if not w:
+ w = 7
+
+ return w
+
+
+def days_in_year(year: int) -> int:
+ if is_leap(year):
+ return DAYS_PER_L_YEAR
+
+ return DAYS_PER_N_YEAR
+
+
+def timestamp(dt: 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: int, utc_offset: int, microseconds: int
+) -> tuple[int, int, int, int, int, int, int]:
+ """
+ Returns a UNIX time as a broken-down time
+ for a particular transition type.
+ """
+ year = EPOCH_YEAR
+ seconds = 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: datetime.datetime | datetime.date, d2: datetime.datetime | datetime.date
+) -> PreciseDiff:
+ """
+ Calculate a precise difference between two datetimes.
+
+ :param d1: The first datetime
+ :param d2: The second datetime
+ """
+ sign = 1
+
+ if d1 == d2:
+ return PreciseDiff(0, 0, 0, 0, 0, 0, 0, 0)
+
+ tzinfo1: datetime.tzinfo | None = (
+ d1.tzinfo if isinstance(d1, datetime.datetime) else None
+ )
+ tzinfo2: datetime.tzinfo | None = (
+ d2.tzinfo if isinstance(d2, datetime.datetime) else None
+ )
+
+ if (
+ tzinfo1 is None
+ and tzinfo2 is not None
+ or tzinfo2 is None
+ and tzinfo1 is not None
+ ):
+ raise ValueError(
+ "Comparison between naive and aware datetimes is not supported"
+ )
+
+ if d1 > d2:
+ d1, d2 = d2, d1
+ sign = -1
+
+ d_diff = 0
+ hour_diff = 0
+ min_diff = 0
+ sec_diff = 0
+ mic_diff = 0
+ total_days = _day_number(d2.year, d2.month, d2.day) - _day_number(
+ d1.year, d1.month, d1.day
+ )
+ in_same_tz = False
+ tz1 = None
+ tz2 = None
+
+ # Trying to figure out the timezone names
+ # If we can't find them, we assume different timezones
+ if tzinfo1 and tzinfo2:
+ tz1 = _get_tzinfo_name(tzinfo1)
+ tz2 = _get_tzinfo_name(tzinfo2)
+
+ in_same_tz = tz1 == tz2 and tz1 is not None
+
+ if isinstance(d2, datetime.datetime):
+ if isinstance(d1, datetime.datetime):
+ # If we are not in the same timezone
+ # we need to adjust
+ #
+ # We also need to adjust if we do not
+ # have variable-length units
+ if not in_same_tz or total_days == 0:
+ offset1 = d1.utcoffset()
+ offset2 = d2.utcoffset()
+
+ if offset1:
+ d1 = d1 - offset1
+
+ if offset2:
+ d2 = d2 - offset2
+
+ hour_diff = d2.hour - d1.hour
+ min_diff = d2.minute - d1.minute
+ sec_diff = d2.second - d1.second
+ mic_diff = d2.microsecond - d1.microsecond
+ else:
+ hour_diff = d2.hour
+ min_diff = d2.minute
+ sec_diff = d2.second
+ mic_diff = d2.microsecond
+
+ if mic_diff < 0:
+ mic_diff += 1000000
+ sec_diff -= 1
+
+ if sec_diff < 0:
+ sec_diff += 60
+ min_diff -= 1
+
+ if min_diff < 0:
+ min_diff += 60
+ hour_diff -= 1
+
+ if hour_diff < 0:
+ hour_diff += 24
+ d_diff -= 1
+
+ y_diff = d2.year - d1.year
+ m_diff = d2.month - d1.month
+ d_diff += d2.day - d1.day
+
+ if d_diff < 0:
+ year = d2.year
+ month = d2.month
+
+ if month == 1:
+ month = 12
+ year -= 1
+ else:
+ month -= 1
+
+ leap = int(is_leap(year))
+
+ days_in_last_month = DAYS_PER_MONTHS[leap][month]
+ days_in_month = DAYS_PER_MONTHS[int(is_leap(d2.year))][d2.month]
+
+ if d_diff < days_in_month - days_in_last_month:
+ # We don't have a full month, we calculate days
+ if days_in_last_month < d1.day:
+ d_diff += d1.day
+ else:
+ d_diff += days_in_last_month
+ elif d_diff == days_in_month - days_in_last_month:
+ # We have exactly a full month
+ # We remove the days difference
+ # and add one to the months difference
+ d_diff = 0
+ m_diff += 1
+ else:
+ # We have a full month
+ d_diff += days_in_last_month
+
+ m_diff -= 1
+
+ if m_diff < 0:
+ m_diff += 12
+ y_diff -= 1
+
+ return PreciseDiff(
+ sign * y_diff,
+ sign * m_diff,
+ sign * d_diff,
+ sign * hour_diff,
+ sign * min_diff,
+ sign * sec_diff,
+ sign * mic_diff,
+ sign * total_days,
+ )
+
+
+def _day_number(year: int, month: int, day: int) -> int:
+ month = (month + 9) % 12
+ year = year - month // 10
+
+ return (
+ 365 * year
+ + year // 4
+ - year // 100
+ + year // 400
+ + (month * 306 + 5) // 10
+ + (day - 1)
+ )
+
+
+def _get_tzinfo_name(tzinfo: datetime.tzinfo | None) -> str | None:
+ if tzinfo is None:
+ return None
+
+ if hasattr(tzinfo, "key"):
+ # zoneinfo timezone
+ return cast(str, cast(zoneinfo.ZoneInfo, tzinfo).key)
+ elif hasattr(tzinfo, "name"):
+ # Pendulum timezone
+ return cast(Timezone, tzinfo).name
+ elif hasattr(tzinfo, "zone"):
+ # pytz timezone
+ return tzinfo.zone # type: ignore
+
+ return None
diff --git a/pendulum/constants.py b/pendulum/constants.py
index 3712df3..a3d2a18 100644
--- a/pendulum/constants.py
+++ b/pendulum/constants.py
@@ -1,107 +1,109 @@
-# 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
+# The day constants
+from __future__ import annotations
+
+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
index 41a9883..4e2655b 100644
--- a/pendulum/date.py
+++ b/pendulum/date.py
@@ -1,891 +1,760 @@
-from __future__ import absolute_import
-from __future__ import division
-
-import calendar
-import math
-
-from datetime import date
-from datetime import timedelta
-
-import pendulum
-
-from .constants import FRIDAY
-from .constants import MONDAY
-from .constants import MONTHS_PER_YEAR
-from .constants import SATURDAY
-from .constants import SUNDAY
-from .constants import THURSDAY
-from .constants import TUESDAY
-from .constants import WEDNESDAY
-from .constants import YEARS_PER_CENTURY
-from .constants import YEARS_PER_DECADE
-from .exceptions import PendulumException
-from .helpers import add_duration
-from .mixins.default import FormattableMixin
-from .period import Period
-
-
-class Date(FormattableMixin, date):
-
- # Names of days of the week
- _days = {
- SUNDAY: "Sunday",
- MONDAY: "Monday",
- TUESDAY: "Tuesday",
- WEDNESDAY: "Wednesday",
- THURSDAY: "Thursday",
- FRIDAY: "Friday",
- SATURDAY: "Saturday",
- }
-
- _MODIFIERS_VALID_UNITS = ["day", "week", "month", "year", "decade", "century"]
-
- # Getters/Setters
-
- def set(self, year=None, month=None, day=None):
- return self.replace(year=year, month=month, day=day)
-
- @property
- def day_of_week(self):
- """
- Returns the day of the week (0-6).
-
- :rtype: int
- """
- return self.isoweekday() % 7
-
- @property
- def day_of_year(self):
- """
- Returns the day of the year (1-366).
-
- :rtype: int
- """
- k = 1 if self.is_leap_year() else 2
-
- return (275 * self.month) // 9 - k * ((self.month + 9) // 12) + self.day - 30
-
- @property
- def week_of_year(self):
- return self.isocalendar()[1]
-
- @property
- def days_in_month(self):
- return calendar.monthrange(self.year, self.month)[1]
-
- @property
- def week_of_month(self):
- first_day_of_month = self.replace(day=1)
-
- return self.week_of_year - first_day_of_month.week_of_year + 1
-
- @property
- def age(self):
- return self.diff(abs=False).in_years()
-
- @property
- def quarter(self):
- return int(math.ceil(self.month / 3))
-
- # String Formatting
-
- def to_date_string(self):
- """
- Format the instance as date.
-
- :rtype: str
- """
- return self.strftime("%Y-%m-%d")
-
- def to_formatted_date_string(self):
- """
- Format the instance as a readable date.
-
- :rtype: str
- """
- return self.strftime("%b %d, %Y")
-
- def __repr__(self):
- return (
- "{klass}("
- "{year}, {month}, {day}"
- ")".format(
- klass=self.__class__.__name__,
- year=self.year,
- month=self.month,
- day=self.day,
- )
- )
-
- # COMPARISONS
-
- def closest(self, dt1, dt2):
- """
- Get the closest date from the instance.
-
- :type dt1: Date or date
- :type dt2: Date or date
-
- :rtype: Date
- """
- dt1 = self.__class__(dt1.year, dt1.month, dt1.day)
- dt2 = self.__class__(dt2.year, dt2.month, dt2.day)
-
- if self.diff(dt1).in_seconds() < self.diff(dt2).in_seconds():
- return dt1
-
- return dt2
-
- def farthest(self, dt1, dt2):
- """
- Get the farthest date from the instance.
-
- :type dt1: Date or date
- :type dt2: Date or date
-
- :rtype: Date
- """
- dt1 = self.__class__(dt1.year, dt1.month, dt1.day)
- dt2 = self.__class__(dt2.year, dt2.month, dt2.day)
-
- if self.diff(dt1).in_seconds() > self.diff(dt2).in_seconds():
- return dt1
-
- return dt2
-
- def is_future(self):
- """
- Determines if the instance is in the future, ie. greater than now.
-
- :rtype: bool
- """
- return self > self.today()
-
- def is_past(self):
- """
- Determines if the instance is in the past, ie. less than now.
-
- :rtype: bool
- """
- return self < self.today()
-
- def is_leap_year(self):
- """
- Determines if the instance is a leap year.
-
- :rtype: bool
- """
- return calendar.isleap(self.year)
-
- def is_long_year(self):
- """
- Determines if the instance is a long year
-
- See link `<https://en.wikipedia.org/wiki/ISO_8601#Week_dates>`_
-
- :rtype: bool
- """
- return Date(self.year, 12, 28).isocalendar()[1] == 53
-
- def is_same_day(self, dt):
- """
- Checks if the passed in date is the same day as the instance current day.
-
- :type dt: Date or date
-
- :rtype: bool
- """
- return self == dt
-
- def is_anniversary(self, dt=None):
- """
- Check if its the anniversary.
-
- Compares the date/month values of the two dates.
-
- :rtype: bool
- """
- if dt is None:
- dt = Date.today()
-
- instance = self.__class__(dt.year, dt.month, dt.day)
-
- return (self.month, self.day) == (instance.month, instance.day)
-
- # the additional method for checking if today is the anniversary day
- # the alias is provided to start using a new name and keep the backward compatibility
- # the old name can be completely replaced with the new in one of the future versions
- is_birthday = is_anniversary
-
- # ADDITIONS AND SUBSTRACTIONS
-
- def add(self, years=0, months=0, weeks=0, days=0):
- """
- Add duration to the instance.
-
- :param years: The number of years
- :type years: int
-
- :param months: The number of months
- :type months: int
-
- :param weeks: The number of weeks
- :type weeks: int
-
- :param days: The number of days
- :type days: int
-
- :rtype: Date
- """
- dt = add_duration(
- date(self.year, self.month, self.day),
- years=years,
- months=months,
- weeks=weeks,
- days=days,
- )
-
- return self.__class__(dt.year, dt.month, dt.day)
-
- def subtract(self, years=0, months=0, weeks=0, days=0):
- """
- Remove duration from the instance.
-
- :param years: The number of years
- :type years: int
-
- :param months: The number of months
- :type months: int
-
- :param weeks: The number of weeks
- :type weeks: int
-
- :param days: The number of days
- :type days: int
-
- :rtype: Date
- """
- return self.add(years=-years, months=-months, weeks=-weeks, days=-days)
-
- def _add_timedelta(self, delta):
- """
- Add timedelta duration to the instance.
-
- :param delta: The timedelta instance
- :type delta: pendulum.Duration or datetime.timedelta
-
- :rtype: Date
- """
- if isinstance(delta, pendulum.Duration):
- return self.add(
- years=delta.years,
- months=delta.months,
- weeks=delta.weeks,
- days=delta.remaining_days,
- )
-
- return self.add(days=delta.days)
-
- def _subtract_timedelta(self, delta):
- """
- Remove timedelta duration from the instance.
-
- :param delta: The timedelta instance
- :type delta: pendulum.Duration or datetime.timedelta
-
- :rtype: Date
- """
- if isinstance(delta, pendulum.Duration):
- return self.subtract(
- years=delta.years,
- months=delta.months,
- weeks=delta.weeks,
- days=delta.remaining_days,
- )
-
- return self.subtract(days=delta.days)
-
- def __add__(self, other):
- if not isinstance(other, timedelta):
- return NotImplemented
-
- return self._add_timedelta(other)
-
- def __sub__(self, other):
- if isinstance(other, timedelta):
- return self._subtract_timedelta(other)
-
- if not isinstance(other, date):
- return NotImplemented
-
- dt = self.__class__(other.year, other.month, other.day)
-
- return dt.diff(self, False)
-
- # DIFFERENCES
-
- def diff(self, dt=None, abs=True):
- """
- Returns the difference between two Date objects as a Period.
-
- :type dt: Date or None
-
- :param abs: Whether to return an absolute interval or not
- :type abs: bool
-
- :rtype: Period
- """
- if dt is None:
- dt = self.today()
-
- return Period(self, Date(dt.year, dt.month, dt.day), absolute=abs)
-
- def diff_for_humans(self, other=None, absolute=False, locale=None):
- """
- Get the difference in a human readable format in the current locale.
-
- When comparing a value in the past to default now:
- 1 day ago
- 5 months ago
-
- When comparing a value in the future to default now:
- 1 day from now
- 5 months from now
-
- When comparing a value in the past to another value:
- 1 day before
- 5 months before
-
- When comparing a value in the future to another value:
- 1 day after
- 5 months after
-
- :type other: Date
-
- :param absolute: removes time difference modifiers ago, after, etc
- :type absolute: bool
-
- :param locale: The locale to use for localization
- :type locale: str
-
- :rtype: str
- """
- is_now = other is None
-
- if is_now:
- other = self.today()
-
- diff = self.diff(other)
-
- return pendulum.format_diff(diff, is_now, absolute, locale)
-
- # MODIFIERS
-
- def start_of(self, unit):
- """
- Returns a copy of the instance with the time reset
- with the following rules:
-
- * day: time to 00:00:00
- * week: date to first day of the week and time to 00:00:00
- * month: date to first day of the month and time to 00:00:00
- * year: date to first day of the year and time to 00:00:00
- * decade: date to first day of the decade and time to 00:00:00
- * century: date to first day of century and time to 00:00:00
-
- :param unit: The unit to reset to
- :type unit: str
-
- :rtype: Date
- """
- if unit not in self._MODIFIERS_VALID_UNITS:
- raise ValueError('Invalid unit "{}" for start_of()'.format(unit))
-
- return getattr(self, "_start_of_{}".format(unit))()
-
- def end_of(self, unit):
- """
- Returns a copy of the instance with the time reset
- with the following rules:
-
- * week: date to last day of the week
- * month: date to last day of the month
- * year: date to last day of the year
- * decade: date to last day of the decade
- * century: date to last day of century
-
- :param unit: The unit to reset to
- :type unit: str
-
- :rtype: Date
- """
- if unit not in self._MODIFIERS_VALID_UNITS:
- raise ValueError('Invalid unit "%s" for end_of()' % unit)
-
- return getattr(self, "_end_of_%s" % unit)()
-
- def _start_of_day(self):
- """
- Compatibility method.
-
- :rtype: Date
- """
- return self
-
- def _end_of_day(self):
- """
- Compatibility method
-
- :rtype: Date
- """
- return self
-
- def _start_of_month(self):
- """
- Reset the date to the first day of the month.
-
- :rtype: Date
- """
- return self.set(self.year, self.month, 1)
-
- def _end_of_month(self):
- """
- Reset the date to the last day of the month.
-
- :rtype: Date
- """
- return self.set(self.year, self.month, self.days_in_month)
-
- def _start_of_year(self):
- """
- Reset the date to the first day of the year.
-
- :rtype: Date
- """
- return self.set(self.year, 1, 1)
-
- def _end_of_year(self):
- """
- Reset the date to the last day of the year.
-
- :rtype: Date
- """
- return self.set(self.year, 12, 31)
-
- def _start_of_decade(self):
- """
- Reset the date to the first day of the decade.
-
- :rtype: Date
- """
- year = self.year - self.year % YEARS_PER_DECADE
-
- return self.set(year, 1, 1)
-
- def _end_of_decade(self):
- """
- Reset the date to the last day of the decade.
-
- :rtype: Date
- """
- year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1
-
- return self.set(year, 12, 31)
-
- def _start_of_century(self):
- """
- Reset the date to the first day of the century.
-
- :rtype: Date
- """
- year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1
-
- return self.set(year, 1, 1)
-
- def _end_of_century(self):
- """
- Reset the date to the last day of the century.
-
- :rtype: Date
- """
- year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY
-
- return self.set(year, 12, 31)
-
- def _start_of_week(self):
- """
- Reset the date to the first day of the week.
-
- :rtype: Date
- """
- dt = self
-
- if self.day_of_week != pendulum._WEEK_STARTS_AT:
- dt = self.previous(pendulum._WEEK_STARTS_AT)
-
- return dt.start_of("day")
-
- def _end_of_week(self):
- """
- Reset the date to the last day of the week.
-
- :rtype: Date
- """
- dt = self
-
- if self.day_of_week != pendulum._WEEK_ENDS_AT:
- dt = self.next(pendulum._WEEK_ENDS_AT)
-
- return dt.end_of("day")
-
- def next(self, day_of_week=None):
- """
- Modify to the next occurrence of a given day of the week.
- If no day_of_week is provided, modify to the next occurrence
- of the current day of the week. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :param day_of_week: The next day of week to reset to.
- :type day_of_week: int or None
-
- :rtype: Date
- """
- if day_of_week is None:
- day_of_week = self.day_of_week
-
- if day_of_week < SUNDAY or day_of_week > SATURDAY:
- raise ValueError("Invalid day of week")
-
- dt = self.add(days=1)
- while dt.day_of_week != day_of_week:
- dt = dt.add(days=1)
-
- return dt
-
- def previous(self, day_of_week=None):
- """
- Modify to the previous occurrence of a given day of the week.
- If no day_of_week is provided, modify to the previous occurrence
- of the current day of the week. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :param day_of_week: The previous day of week to reset to.
- :type day_of_week: int or None
-
- :rtype: Date
- """
- if day_of_week is None:
- day_of_week = self.day_of_week
-
- if day_of_week < SUNDAY or day_of_week > SATURDAY:
- raise ValueError("Invalid day of week")
-
- dt = self.subtract(days=1)
- while dt.day_of_week != day_of_week:
- dt = dt.subtract(days=1)
-
- return dt
-
- def first_of(self, unit, day_of_week=None):
- """
- Returns an instance set to the first occurrence
- of a given day of the week in the current unit.
- If no day_of_week is provided, modify to the first day of the unit.
- Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- Supported units are month, quarter and year.
-
- :param unit: The unit to use
- :type unit: str
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- if unit not in ["month", "quarter", "year"]:
- raise ValueError('Invalid unit "{}" for first_of()'.format(unit))
-
- return getattr(self, "_first_of_{}".format(unit))(day_of_week)
-
- def last_of(self, unit, day_of_week=None):
- """
- Returns an instance set to the last occurrence
- of a given day of the week in the current unit.
- If no day_of_week is provided, modify to the last day of the unit.
- Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- Supported units are month, quarter and year.
-
- :param unit: The unit to use
- :type unit: str
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- if unit not in ["month", "quarter", "year"]:
- raise ValueError('Invalid unit "{}" for first_of()'.format(unit))
-
- return getattr(self, "_last_of_{}".format(unit))(day_of_week)
-
- def nth_of(self, unit, nth, day_of_week):
- """
- Returns a new instance set to the given occurrence
- of a given day of the week in the current unit.
- If the calculated occurrence is outside the scope of the current unit,
- then raise an error. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- Supported units are month, quarter and year.
-
- :param unit: The unit to use
- :type unit: str
-
- :type nth: int
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- if unit not in ["month", "quarter", "year"]:
- raise ValueError('Invalid unit "{}" for first_of()'.format(unit))
-
- dt = getattr(self, "_nth_of_{}".format(unit))(nth, day_of_week)
- if dt is False:
- raise PendulumException(
- "Unable to find occurence {} of {} in {}".format(
- nth, self._days[day_of_week], unit
- )
- )
-
- return dt
-
- def _first_of_month(self, day_of_week):
- """
- Modify to the first occurrence of a given day of the week
- in the current month. If no day_of_week is provided,
- modify to the first day of the month. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type day_of_week: int
-
- :rtype: Date
- """
- dt = self
-
- if day_of_week is None:
- return dt.set(day=1)
-
- month = calendar.monthcalendar(dt.year, dt.month)
-
- calendar_day = (day_of_week - 1) % 7
-
- if month[0][calendar_day] > 0:
- day_of_month = month[0][calendar_day]
- else:
- day_of_month = month[1][calendar_day]
-
- return dt.set(day=day_of_month)
-
- def _last_of_month(self, day_of_week=None):
- """
- Modify to the last occurrence of a given day of the week
- in the current month. If no day_of_week is provided,
- modify to the last day of the month. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- dt = self
-
- if day_of_week is None:
- return dt.set(day=self.days_in_month)
-
- month = calendar.monthcalendar(dt.year, dt.month)
-
- calendar_day = (day_of_week - 1) % 7
-
- if month[-1][calendar_day] > 0:
- day_of_month = month[-1][calendar_day]
- else:
- day_of_month = month[-2][calendar_day]
-
- return dt.set(day=day_of_month)
-
- def _nth_of_month(self, nth, day_of_week):
- """
- Modify to the given occurrence of a given day of the week
- in the current month. If the calculated occurrence is outside,
- the scope of the current month, then return False and no
- modifications are made. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type nth: int
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- if nth == 1:
- return self.first_of("month", day_of_week)
-
- dt = self.first_of("month")
- check = dt.format("YYYY-MM")
- for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
- dt = dt.next(day_of_week)
-
- if dt.format("YYYY-MM") == check:
- return self.set(day=dt.day)
-
- return False
-
- def _first_of_quarter(self, day_of_week=None):
- """
- Modify to the first occurrence of a given day of the week
- in the current quarter. If no day_of_week is provided,
- modify to the first day of the quarter. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- return self.set(self.year, self.quarter * 3 - 2, 1).first_of(
- "month", day_of_week
- )
-
- def _last_of_quarter(self, day_of_week=None):
- """
- Modify to the last occurrence of a given day of the week
- in the current quarter. If no day_of_week is provided,
- modify to the last day of the quarter. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- return self.set(self.year, self.quarter * 3, 1).last_of("month", day_of_week)
-
- def _nth_of_quarter(self, nth, day_of_week):
- """
- Modify to the given occurrence of a given day of the week
- in the current quarter. If the calculated occurrence is outside,
- the scope of the current quarter, then return False and no
- modifications are made. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type nth: int
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- if nth == 1:
- return self.first_of("quarter", day_of_week)
-
- dt = self.replace(self.year, self.quarter * 3, 1)
- last_month = dt.month
- year = dt.year
- dt = dt.first_of("quarter")
- for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
- dt = dt.next(day_of_week)
-
- if last_month < dt.month or year != dt.year:
- return False
-
- return self.set(self.year, dt.month, dt.day)
-
- def _first_of_year(self, day_of_week=None):
- """
- Modify to the first occurrence of a given day of the week
- in the current year. If no day_of_week is provided,
- modify to the first day of the year. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- return self.set(month=1).first_of("month", day_of_week)
-
- def _last_of_year(self, day_of_week=None):
- """
- Modify to the last occurrence of a given day of the week
- in the current year. If no day_of_week is provided,
- modify to the last day of the year. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week)
-
- def _nth_of_year(self, nth, day_of_week):
- """
- Modify to the given occurrence of a given day of the week
- in the current year. If the calculated occurrence is outside,
- the scope of the current year, then return False and no
- modifications are made. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type nth: int
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- if nth == 1:
- return self.first_of("year", day_of_week)
-
- dt = self.first_of("year")
- year = dt.year
- for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
- dt = dt.next(day_of_week)
-
- if year != dt.year:
- return False
-
- return self.set(self.year, dt.month, dt.day)
-
- def average(self, dt=None):
- """
- Modify the current instance to the average
- of a given instance (default now) and the current instance.
-
- :type dt: Date or date
-
- :rtype: Date
- """
- if dt is None:
- dt = Date.today()
-
- return self.add(days=int(self.diff(dt, False).in_days() / 2))
-
- # Native methods override
-
- @classmethod
- def today(cls):
- return pendulum.today().date()
-
- @classmethod
- def fromtimestamp(cls, t):
- dt = super(Date, cls).fromtimestamp(t)
-
- return cls(dt.year, dt.month, dt.day)
-
- @classmethod
- def fromordinal(cls, n):
- dt = super(Date, cls).fromordinal(n)
-
- return cls(dt.year, dt.month, dt.day)
-
- def replace(self, year=None, month=None, day=None):
- year = year if year is not None else self.year
- month = month if month is not None else self.month
- day = day if day is not None else self.day
-
- return self.__class__(year, month, day)
+from __future__ import annotations
+
+import calendar
+import math
+
+from datetime import date
+from datetime import datetime
+from datetime import timedelta
+from typing import NoReturn
+from typing import cast
+from typing import overload
+
+import pendulum
+
+from pendulum.constants import FRIDAY
+from pendulum.constants import MONDAY
+from pendulum.constants import MONTHS_PER_YEAR
+from pendulum.constants import SATURDAY
+from pendulum.constants import SUNDAY
+from pendulum.constants import THURSDAY
+from pendulum.constants import TUESDAY
+from pendulum.constants import WEDNESDAY
+from pendulum.constants import YEARS_PER_CENTURY
+from pendulum.constants import YEARS_PER_DECADE
+from pendulum.exceptions import PendulumException
+from pendulum.helpers import add_duration
+from pendulum.interval import Interval
+from pendulum.mixins.default import FormattableMixin
+
+
+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: int | None = None, month: int | None = None, day: int | None = None
+ ) -> Date:
+ return self.replace(year=year, month=month, day=day)
+
+ @property
+ def day_of_week(self) -> int:
+ """
+ Returns the day of the week (0-6).
+ """
+ return self.isoweekday() % 7
+
+ @property
+ def day_of_year(self) -> int:
+ """
+ Returns the day of the year (1-366).
+ """
+ k = 1 if self.is_leap_year() else 2
+
+ return (275 * self.month) // 9 - k * ((self.month + 9) // 12) + self.day - 30
+
+ @property
+ def week_of_year(self) -> int:
+ return self.isocalendar()[1]
+
+ @property
+ def days_in_month(self) -> int:
+ return calendar.monthrange(self.year, self.month)[1]
+
+ @property
+ def week_of_month(self) -> int:
+ 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) -> int:
+ return self.diff(abs=False).in_years()
+
+ @property
+ def quarter(self) -> int:
+ return int(math.ceil(self.month / 3))
+
+ # String Formatting
+
+ def to_date_string(self) -> str:
+ """
+ Format the instance as date.
+
+ :rtype: str
+ """
+ return self.strftime("%Y-%m-%d")
+
+ def to_formatted_date_string(self) -> str:
+ """
+ Format the instance as a readable date.
+
+ :rtype: str
+ """
+ return self.strftime("%b %d, %Y")
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}({self.year}, {self.month}, {self.day})"
+
+ # COMPARISONS
+
+ def closest(self, dt1: date, dt2: date) -> Date:
+ """
+ Get the closest date from the instance.
+ """
+ dt1 = self.__class__(dt1.year, dt1.month, dt1.day)
+ dt2 = self.__class__(dt2.year, dt2.month, dt2.day)
+
+ if self.diff(dt1).in_seconds() < self.diff(dt2).in_seconds():
+ return dt1
+
+ return dt2
+
+ def farthest(self, dt1: date, dt2: date) -> Date:
+ """
+ Get the farthest date from the instance.
+ """
+ dt1 = self.__class__(dt1.year, dt1.month, dt1.day)
+ dt2 = self.__class__(dt2.year, dt2.month, dt2.day)
+
+ if self.diff(dt1).in_seconds() > self.diff(dt2).in_seconds():
+ return dt1
+
+ return dt2
+
+ def is_future(self) -> bool:
+ """
+ Determines if the instance is in the future, ie. greater than now.
+ """
+ return self > self.today()
+
+ def is_past(self) -> bool:
+ """
+ Determines if the instance is in the past, ie. less than now.
+ """
+ return self < self.today()
+
+ def is_leap_year(self) -> bool:
+ """
+ Determines if the instance is a leap year.
+ """
+ return calendar.isleap(self.year)
+
+ def is_long_year(self) -> bool:
+ """
+ Determines if the instance is a long year
+
+ See link `<https://en.wikipedia.org/wiki/ISO_8601#Week_dates>`_
+ """
+ return Date(self.year, 12, 28).isocalendar()[1] == 53
+
+ def is_same_day(self, dt: date) -> bool:
+ """
+ Checks if the passed in date is the same day as the instance current day.
+ """
+ return self == dt
+
+ def is_anniversary(self, dt: date | None = None) -> bool:
+ """
+ Check if it's the anniversary.
+
+ Compares the date/month values of the two dates.
+ """
+ if dt is None:
+ dt = 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 SUBTRACTIONS
+
+ def add(
+ self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0
+ ) -> Date:
+ """
+ Add duration to the instance.
+
+ :param years: The number of years
+ :param months: The number of months
+ :param weeks: The number of weeks
+ :param days: The number of days
+ """
+ dt = add_duration(
+ date(self.year, self.month, self.day),
+ years=years,
+ months=months,
+ weeks=weeks,
+ days=days,
+ )
+
+ return self.__class__(dt.year, dt.month, dt.day)
+
+ def subtract(
+ self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0
+ ) -> Date:
+ """
+ Remove duration from the instance.
+
+ :param years: The number of years
+ :param months: The number of months
+ :param weeks: The number of weeks
+ :param days: The number of days
+ """
+ return self.add(years=-years, months=-months, weeks=-weeks, days=-days)
+
+ def _add_timedelta(self, delta: timedelta) -> Date:
+ """
+ Add timedelta duration to the instance.
+
+ :param delta: The timedelta instance
+ """
+ if isinstance(delta, pendulum.Duration):
+ return self.add(
+ years=delta.years,
+ months=delta.months,
+ weeks=delta.weeks,
+ days=delta.remaining_days,
+ )
+
+ return self.add(days=delta.days)
+
+ def _subtract_timedelta(self, delta: timedelta) -> Date:
+ """
+ Remove timedelta duration from the instance.
+
+ :param delta: The timedelta instance
+ """
+ if isinstance(delta, pendulum.Duration):
+ return self.subtract(
+ years=delta.years,
+ months=delta.months,
+ weeks=delta.weeks,
+ days=delta.remaining_days,
+ )
+
+ return self.subtract(days=delta.days)
+
+ def __add__(self, other: timedelta) -> Date:
+ if not isinstance(other, timedelta):
+ return NotImplemented
+
+ return self._add_timedelta(other)
+
+ @overload
+ def __sub__(self, delta: timedelta) -> Date:
+ ...
+
+ @overload
+ def __sub__(self, dt: datetime) -> NoReturn:
+ ...
+
+ @overload
+ def __sub__(self, dt: Date) -> Interval:
+ ...
+
+ def __sub__(self, other: timedelta | date) -> Date | Interval:
+ if isinstance(other, timedelta):
+ return self._subtract_timedelta(other)
+
+ if not isinstance(other, date):
+ return NotImplemented
+
+ dt = self.__class__(other.year, other.month, other.day)
+
+ return dt.diff(self, False)
+
+ # DIFFERENCES
+
+ def diff(self, dt: date | None = None, abs: bool = True) -> Interval:
+ """
+ Returns the difference between two Date objects as an Interval.
+
+ :param dt: The date to compare to (defaults to today)
+ :param abs: Whether to return an absolute interval or not
+ """
+ if dt is None:
+ dt = self.today()
+
+ return Interval(self, Date(dt.year, dt.month, dt.day), absolute=abs)
+
+ def diff_for_humans(
+ self,
+ other: date | None = None,
+ absolute: bool = False,
+ locale: str | None = None,
+ ) -> str:
+ """
+ Get the difference in a human readable format in the current locale.
+
+ When comparing a value in the past to default now:
+ 1 day ago
+ 5 months ago
+
+ When comparing a value in the future to default now:
+ 1 day from now
+ 5 months from now
+
+ When comparing a value in the past to another value:
+ 1 day before
+ 5 months before
+
+ When comparing a value in the future to another value:
+ 1 day after
+ 5 months after
+
+ :param other: The date to compare to (defaults to today)
+ :param absolute: removes time difference modifiers ago, after, etc
+ :param locale: The locale to use for localization
+ """
+ is_now = other is None
+
+ if is_now:
+ other = self.today()
+
+ diff = self.diff(other)
+
+ return pendulum.format_diff(diff, is_now, absolute, locale)
+
+ # MODIFIERS
+
+ def start_of(self, unit: str) -> Date:
+ """
+ Returns a copy of the instance with the time reset
+ with the following rules:
+
+ * day: time to 00:00:00
+ * week: date to first day of the week and time to 00:00:00
+ * month: date to first day of the month and time to 00:00:00
+ * year: date to first day of the year and time to 00:00:00
+ * decade: date to first day of the decade and time to 00:00:00
+ * century: date to first day of century and time to 00:00:00
+
+ :param unit: The unit to reset to
+ """
+ if unit not in self._MODIFIERS_VALID_UNITS:
+ raise ValueError(f'Invalid unit "{unit}" for start_of()')
+
+ return cast(Date, getattr(self, f"_start_of_{unit}")())
+
+ def end_of(self, unit: str) -> Date:
+ """
+ Returns a copy of the instance with the time reset
+ with the following rules:
+
+ * week: date to last day of the week
+ * month: date to last day of the month
+ * year: date to last day of the year
+ * decade: date to last day of the decade
+ * century: date to last day of century
+
+ :param unit: The unit to reset to
+ """
+ if unit not in self._MODIFIERS_VALID_UNITS:
+ raise ValueError(f'Invalid unit "{unit}" for end_of()')
+
+ return cast(Date, getattr(self, f"_end_of_{unit}")())
+
+ def _start_of_day(self) -> Date:
+ """
+ Compatibility method.
+ """
+ return self
+
+ def _end_of_day(self) -> Date:
+ """
+ Compatibility method
+ """
+ return self
+
+ def _start_of_month(self) -> Date:
+ """
+ Reset the date to the first day of the month.
+ """
+ return self.set(self.year, self.month, 1)
+
+ def _end_of_month(self) -> Date:
+ """
+ Reset the date to the last day of the month.
+ """
+ return self.set(self.year, self.month, self.days_in_month)
+
+ def _start_of_year(self) -> Date:
+ """
+ Reset the date to the first day of the year.
+ """
+ return self.set(self.year, 1, 1)
+
+ def _end_of_year(self) -> Date:
+ """
+ Reset the date to the last day of the year.
+ """
+ return self.set(self.year, 12, 31)
+
+ def _start_of_decade(self) -> Date:
+ """
+ Reset the date to the first day of the decade.
+ """
+ year = self.year - self.year % YEARS_PER_DECADE
+
+ return self.set(year, 1, 1)
+
+ def _end_of_decade(self) -> Date:
+ """
+ Reset the date to the last day of the decade.
+ """
+ year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1
+
+ return self.set(year, 12, 31)
+
+ def _start_of_century(self) -> Date:
+ """
+ Reset the date to the first day of the century.
+ """
+ year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1
+
+ return self.set(year, 1, 1)
+
+ def _end_of_century(self) -> Date:
+ """
+ Reset the date to the last day of the century.
+ """
+ year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY
+
+ return self.set(year, 12, 31)
+
+ def _start_of_week(self) -> Date:
+ """
+ Reset the date to the first day of the week.
+ """
+ dt = self
+
+ if self.day_of_week != pendulum._WEEK_STARTS_AT:
+ dt = self.previous(pendulum._WEEK_STARTS_AT)
+
+ return dt.start_of("day")
+
+ def _end_of_week(self) -> Date:
+ """
+ Reset the date to the last day of the week.
+ """
+ dt = self
+
+ if self.day_of_week != pendulum._WEEK_ENDS_AT:
+ dt = self.next(pendulum._WEEK_ENDS_AT)
+
+ return dt.end_of("day")
+
+ def next(self, day_of_week: int | None = None) -> Date:
+ """
+ Modify to the next occurrence of a given day of the week.
+ If no day_of_week is provided, modify to the next occurrence
+ of the current day of the week. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+
+ :param day_of_week: The next day of week to reset to.
+ """
+ if day_of_week is None:
+ day_of_week = self.day_of_week
+
+ if day_of_week < 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: int | None = None) -> Date:
+ """
+ Modify to the previous occurrence of a given day of the week.
+ If no day_of_week is provided, modify to the previous occurrence
+ of the current day of the week. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+
+ :param day_of_week: The previous day of week to reset to.
+ """
+ if day_of_week is None:
+ day_of_week = self.day_of_week
+
+ if day_of_week < 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: str, day_of_week: int | None = None) -> Date:
+ """
+ Returns an instance set to the first occurrence
+ of a given day of the week in the current unit.
+ If no day_of_week is provided, modify to the first day of the unit.
+ Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY.
+
+ Supported units are month, quarter and year.
+
+ :param unit: The unit to use
+ :param day_of_week: The day of week to reset to.
+ """
+ if unit not in ["month", "quarter", "year"]:
+ raise ValueError(f'Invalid unit "{unit}" for first_of()')
+
+ return cast(Date, getattr(self, f"_first_of_{unit}")(day_of_week))
+
+ def last_of(self, unit: str, day_of_week: int | None = None) -> Date:
+ """
+ Returns an instance set to the last occurrence
+ of a given day of the week in the current unit.
+ If no day_of_week is provided, modify to the last day of the unit.
+ Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY.
+
+ Supported units are month, quarter and year.
+
+ :param unit: The unit to use
+ :param day_of_week: The day of week to reset to.
+ """
+ if unit not in ["month", "quarter", "year"]:
+ raise ValueError(f'Invalid unit "{unit}" for first_of()')
+
+ return cast(Date, getattr(self, f"_last_of_{unit}")(day_of_week))
+
+ def nth_of(self, unit: str, nth: int, day_of_week: int) -> Date:
+ """
+ Returns a new instance set to the given occurrence
+ of a given day of the week in the current unit.
+ If the calculated occurrence is outside the scope of the current unit,
+ then raise an error. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+
+ Supported units are month, quarter and year.
+
+ :param unit: The unit to use
+ :param nth: The occurrence to use
+ :param day_of_week: The day of week to set to.
+ """
+ if unit not in ["month", "quarter", "year"]:
+ raise ValueError(f'Invalid unit "{unit}" for first_of()')
+
+ dt = cast(Date, getattr(self, f"_nth_of_{unit}")(nth, day_of_week))
+ if not dt:
+ raise PendulumException(
+ f"Unable to find occurence {nth}"
+ f" of {self._days[day_of_week]} in {unit}"
+ )
+
+ return dt
+
+ def _first_of_month(self, day_of_week: int) -> Date:
+ """
+ Modify to the first occurrence of a given day of the week
+ in the current month. If no day_of_week is provided,
+ modify to the first day of the month. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+
+ :param day_of_week: The day of week to set to.
+ """
+ dt = self
+
+ if day_of_week is None:
+ return dt.set(day=1)
+
+ month = calendar.monthcalendar(dt.year, dt.month)
+
+ calendar_day = (day_of_week - 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: int | None = None) -> Date:
+ """
+ Modify to the last occurrence of a given day of the week
+ in the current month. If no day_of_week is provided,
+ modify to the last day of the month. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+
+ :param day_of_week: The day of week to set to.
+ """
+ dt = self
+
+ if day_of_week is None:
+ return dt.set(day=self.days_in_month)
+
+ month = calendar.monthcalendar(dt.year, dt.month)
+
+ calendar_day = (day_of_week - 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: int, day_of_week: int) -> Date | None:
+ """
+ Modify to the given occurrence of a given day of the week
+ in the current month. If the calculated occurrence is outside,
+ the scope of the current month, then return False and no
+ modifications are made. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+ """
+ if nth == 1:
+ return self.first_of("month", day_of_week)
+
+ dt = self.first_of("month")
+ check = dt.format("YYYY-MM")
+ for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
+ dt = dt.next(day_of_week)
+
+ if dt.format("YYYY-MM") == check:
+ return self.set(day=dt.day)
+
+ return None
+
+ def _first_of_quarter(self, day_of_week: int | None = None) -> Date:
+ """
+ Modify to the first occurrence of a given day of the week
+ in the current quarter. If no day_of_week is provided,
+ modify to the first day of the quarter. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+ """
+ return self.set(self.year, self.quarter * 3 - 2, 1).first_of(
+ "month", day_of_week
+ )
+
+ def _last_of_quarter(self, day_of_week: int | None = None) -> Date:
+ """
+ Modify to the last occurrence of a given day of the week
+ in the current quarter. If no day_of_week is provided,
+ modify to the last day of the quarter. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+ """
+ return self.set(self.year, self.quarter * 3, 1).last_of("month", day_of_week)
+
+ def _nth_of_quarter(self, nth: int, day_of_week: int) -> Date | None:
+ """
+ Modify to the given occurrence of a given day of the week
+ in the current quarter. If the calculated occurrence is outside,
+ the scope of the current quarter, then return False and no
+ modifications are made. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+ """
+ if nth == 1:
+ return self.first_of("quarter", day_of_week)
+
+ dt = self.replace(self.year, self.quarter * 3, 1)
+ last_month = dt.month
+ year = dt.year
+ dt = dt.first_of("quarter")
+ for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
+ dt = dt.next(day_of_week)
+
+ if last_month < dt.month or year != dt.year:
+ return None
+
+ return self.set(self.year, dt.month, dt.day)
+
+ def _first_of_year(self, day_of_week: int | None = None) -> Date:
+ """
+ Modify to the first occurrence of a given day of the week
+ in the current year. If no day_of_week is provided,
+ modify to the first day of the year. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+ """
+ return self.set(month=1).first_of("month", day_of_week)
+
+ def _last_of_year(self, day_of_week: int | None = None) -> Date:
+ """
+ Modify to the last occurrence of a given day of the week
+ in the current year. If no day_of_week is provided,
+ modify to the last day of the year. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+ """
+ return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week)
+
+ def _nth_of_year(self, nth: int, day_of_week: int) -> Date | None:
+ """
+ Modify to the given occurrence of a given day of the week
+ in the current year. If the calculated occurrence is outside,
+ the scope of the current year, then return False and no
+ modifications are made. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+ """
+ if nth == 1:
+ return self.first_of("year", day_of_week)
+
+ dt = self.first_of("year")
+ year = dt.year
+ for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
+ dt = dt.next(day_of_week)
+
+ if year != dt.year:
+ return None
+
+ return self.set(self.year, dt.month, dt.day)
+
+ def average(self, dt: date | None = None) -> Date:
+ """
+ Modify the current instance to the average
+ of a given instance (default now) and the current instance.
+ """
+ if dt is None:
+ dt = Date.today()
+
+ return self.add(days=int(self.diff(dt, False).in_days() / 2))
+
+ # Native methods override
+
+ @classmethod
+ def today(cls) -> Date:
+ dt = date.today()
+
+ return cls(dt.year, dt.month, dt.day)
+
+ @classmethod
+ def fromtimestamp(cls, t: float) -> Date:
+ dt = super().fromtimestamp(t)
+
+ return cls(dt.year, dt.month, dt.day)
+
+ @classmethod
+ def fromordinal(cls, n: int) -> Date:
+ dt = super().fromordinal(n)
+
+ return cls(dt.year, dt.month, dt.day)
+
+ def replace(
+ self,
+ year: int | None = None,
+ month: int | None = None,
+ day: int | None = None,
+ ) -> Date:
+ 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
index 6f1d5cf..52ad3cc 100644
--- a/pendulum/datetime.py
+++ b/pendulum/datetime.py
@@ -1,1563 +1,1381 @@
-# -*- 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)
+from __future__ import annotations
+
+import calendar
+import datetime
+
+from typing import TYPE_CHECKING
+from typing import Any
+from typing import Callable
+from typing import Optional
+from typing import cast
+from typing import overload
+
+import pendulum
+
+from pendulum.constants import ATOM
+from pendulum.constants import COOKIE
+from pendulum.constants import MINUTES_PER_HOUR
+from pendulum.constants import MONTHS_PER_YEAR
+from pendulum.constants import RFC822
+from pendulum.constants import RFC850
+from pendulum.constants import RFC1036
+from pendulum.constants import RFC1123
+from pendulum.constants import RFC2822
+from pendulum.constants import RSS
+from pendulum.constants import SATURDAY
+from pendulum.constants import SECONDS_PER_DAY
+from pendulum.constants import SECONDS_PER_MINUTE
+from pendulum.constants import SUNDAY
+from pendulum.constants import W3C
+from pendulum.constants import YEARS_PER_CENTURY
+from pendulum.constants import YEARS_PER_DECADE
+from pendulum.date import Date
+from pendulum.exceptions import PendulumException
+from pendulum.helpers import add_duration
+from pendulum.interval import Interval
+from pendulum.time import Time
+from pendulum.tz import UTC
+from pendulum.tz import local_timezone
+from pendulum.tz.timezone import FixedTimezone
+from pendulum.tz.timezone import Timezone
+from pendulum.utils._compat import PY38
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+
+class DateTime(datetime.datetime, Date):
+ EPOCH: DateTime
+
+ # Formats
+
+ _FORMATS: dict[str, str | Callable[[datetime.datetime], str]] = {
+ "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,
+ }
+
+ _MODIFIERS_VALID_UNITS: list[str] = [
+ "second",
+ "minute",
+ "hour",
+ "day",
+ "week",
+ "month",
+ "year",
+ "decade",
+ "century",
+ ]
+
+ _EPOCH: datetime.datetime = datetime.datetime(1970, 1, 1, tzinfo=UTC)
+
+ @classmethod
+ def create(
+ cls,
+ year: int,
+ month: int,
+ day: int,
+ hour: int = 0,
+ minute: int = 0,
+ second: int = 0,
+ microsecond: int = 0,
+ tz: str | float | Timezone | FixedTimezone | None | datetime.tzinfo = UTC,
+ fold: int = 1,
+ raise_on_unknown_times: bool = False,
+ ) -> DateTime:
+ """
+ Creates a new DateTime instance from a specific date and time.
+ """
+ if tz is not None:
+ tz = pendulum._safe_timezone(tz)
+
+ dt = datetime.datetime(
+ year, month, day, hour, minute, second, microsecond, fold=fold
+ )
+
+ if tz is not None:
+ dt = tz.convert(dt, raise_on_unknown_times=raise_on_unknown_times)
+
+ return cls(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ tzinfo=dt.tzinfo,
+ fold=dt.fold,
+ )
+
+ @overload
+ @classmethod
+ def now(cls, tz: datetime.tzinfo | None = None) -> DateTime:
+ ...
+
+ @overload
+ @classmethod
+ def now(cls, tz: str | Timezone | FixedTimezone | None = None) -> DateTime:
+ ...
+
+ @classmethod
+ def now(
+ cls, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = None
+ ) -> DateTime:
+ """
+ Get a DateTime instance for the current date and time.
+ """
+ if tz is None or tz == "local":
+ dt = datetime.datetime.now(local_timezone())
+ elif tz is UTC or tz == "UTC":
+ dt = datetime.datetime.now(UTC)
+ else:
+ dt = datetime.datetime.now(UTC)
+ tz = pendulum._safe_timezone(tz)
+ dt = dt.astimezone(tz)
+
+ return cls(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ tzinfo=dt.tzinfo,
+ fold=dt.fold,
+ )
+
+ @classmethod
+ def utcnow(cls) -> DateTime:
+ """
+ Get a DateTime instance for the current date and time in UTC.
+ """
+ return cls.now(UTC)
+
+ @classmethod
+ def today(cls) -> DateTime:
+ return cls.now()
+
+ @classmethod
+ def strptime(cls, time: str, fmt: str) -> DateTime:
+ return pendulum.instance(datetime.datetime.strptime(time, fmt))
+
+ # Getters/Setters
+
+ def set(
+ self,
+ year: int | None = None,
+ month: int | None = None,
+ day: int | None = None,
+ hour: int | None = None,
+ minute: int | None = None,
+ second: int | None = None,
+ microsecond: int | None = None,
+ tz: str | float | Timezone | FixedTimezone | datetime.tzinfo | None = None,
+ ) -> DateTime:
+ 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 DateTime.create(
+ year, month, day, hour, minute, second, microsecond, tz=tz
+ )
+
+ @property
+ def float_timestamp(self) -> float:
+ return self.timestamp()
+
+ @property
+ def int_timestamp(self) -> int:
+ # Workaround needed to avoid inaccuracy
+ # for far into the future datetimes
+ dt = datetime.datetime(
+ self.year,
+ self.month,
+ self.day,
+ self.hour,
+ self.minute,
+ self.second,
+ self.microsecond,
+ tzinfo=self.tzinfo,
+ fold=self.fold,
+ )
+
+ delta = dt - self._EPOCH
+
+ return delta.days * SECONDS_PER_DAY + delta.seconds
+
+ @property
+ def offset(self) -> int | None:
+ return self.get_offset()
+
+ @property
+ def offset_hours(self) -> float | None:
+ offset = self.get_offset()
+
+ if offset is None:
+ return None
+
+ return offset / SECONDS_PER_MINUTE / MINUTES_PER_HOUR
+
+ @property
+ def timezone(self) -> Timezone | FixedTimezone | None:
+ if not isinstance(self.tzinfo, (Timezone, FixedTimezone)):
+ return None
+
+ return self.tzinfo
+
+ @property
+ def tz(self) -> Timezone | FixedTimezone | None:
+ return self.timezone
+
+ @property
+ def timezone_name(self) -> str | None:
+ tz = self.timezone
+
+ if tz is None:
+ return None
+
+ return tz.name
+
+ @property
+ def age(self) -> int:
+ return self.date().diff(self.now(self.tz).date(), abs=False).in_years()
+
+ def is_local(self) -> bool:
+ return self.offset == self.in_timezone(pendulum.local_timezone()).offset
+
+ def is_utc(self) -> bool:
+ return self.offset == 0
+
+ def is_dst(self) -> bool:
+ return self.dst() != datetime.timedelta()
+
+ def get_offset(self) -> int | None:
+ utcoffset = self.utcoffset()
+ if utcoffset is None:
+ return None
+
+ return int(utcoffset.total_seconds())
+
+ def date(self) -> Date:
+ return Date(self.year, self.month, self.day)
+
+ def time(self) -> Time:
+ return Time(self.hour, self.minute, self.second, self.microsecond)
+
+ def naive(self) -> DateTime:
+ """
+ Return the DateTime without timezone information.
+ """
+ return self.__class__(
+ self.year,
+ self.month,
+ self.day,
+ self.hour,
+ self.minute,
+ self.second,
+ self.microsecond,
+ )
+
+ def on(self, year: int, month: int, day: int) -> DateTime:
+ """
+ Returns a new instance with the current date set to a different date.
+ """
+ return self.set(year=int(year), month=int(month), day=int(day))
+
+ def at(
+ self, hour: int, minute: int = 0, second: int = 0, microsecond: int = 0
+ ) -> DateTime:
+ """
+ Returns a new instance with the current time to a different time.
+ """
+ return self.set(
+ hour=hour, minute=minute, second=second, microsecond=microsecond
+ )
+
+ def in_timezone(self, tz: str | Timezone | FixedTimezone) -> DateTime:
+ """
+ Set the instance's timezone from a string or object.
+ """
+ tz = pendulum._safe_timezone(tz)
+
+ dt = self
+ if not self.timezone:
+ dt = dt.replace(fold=1)
+
+ return cast(DateTime, tz.convert(dt))
+
+ def in_tz(self, tz: str | Timezone | FixedTimezone) -> DateTime:
+ """
+ Set the instance's timezone from a string or object.
+ """
+ return self.in_timezone(tz)
+
+ # STRING FORMATTING
+
+ def to_time_string(self) -> str:
+ """
+ Format the instance as time.
+ """
+ return self.format("HH:mm:ss")
+
+ def to_datetime_string(self) -> str:
+ """
+ Format the instance as date and time.
+ """
+ return self.format("YYYY-MM-DD HH:mm:ss")
+
+ def to_day_datetime_string(self) -> str:
+ """
+ Format the instance as day, date and time (in english).
+ """
+ return self.format("ddd, MMM D, YYYY h:mm A", locale="en")
+
+ def to_atom_string(self) -> str:
+ """
+ Format the instance as ATOM.
+ """
+ return self._to_string("atom")
+
+ def to_cookie_string(self) -> str:
+ """
+ Format the instance as COOKIE.
+ """
+ return self._to_string("cookie", locale="en")
+
+ def to_iso8601_string(self) -> str:
+ """
+ Format the instance as ISO 8601.
+ """
+ string = self._to_string("iso8601")
+
+ if self.tz and self.tz.name == "UTC":
+ string = string.replace("+00:00", "Z")
+
+ return string
+
+ def to_rfc822_string(self) -> str:
+ """
+ Format the instance as RFC 822.
+ """
+ return self._to_string("rfc822")
+
+ def to_rfc850_string(self) -> str:
+ """
+ Format the instance as RFC 850.
+ """
+ return self._to_string("rfc850")
+
+ def to_rfc1036_string(self) -> str:
+ """
+ Format the instance as RFC 1036.
+ """
+ return self._to_string("rfc1036")
+
+ def to_rfc1123_string(self) -> str:
+ """
+ Format the instance as RFC 1123.
+ """
+ return self._to_string("rfc1123")
+
+ def to_rfc2822_string(self) -> str:
+ """
+ Format the instance as RFC 2822.
+ """
+ return self._to_string("rfc2822")
+
+ def to_rfc3339_string(self) -> str:
+ """
+ Format the instance as RFC 3339.
+ """
+ return self._to_string("rfc3339")
+
+ def to_rss_string(self) -> str:
+ """
+ Format the instance as RSS.
+ """
+ return self._to_string("rss")
+
+ def to_w3c_string(self) -> str:
+ """
+ Format the instance as W3C.
+ """
+ return self._to_string("w3c")
+
+ def _to_string(self, fmt: str, locale: str | None = None) -> str:
+ """
+ Format the instance to a common string format.
+ """
+ if fmt not in self._FORMATS:
+ raise ValueError(f"Format [{fmt}] is not supported")
+
+ fmt_value = self._FORMATS[fmt]
+ if callable(fmt_value):
+ return fmt_value(self)
+
+ return self.format(fmt_value, locale=locale)
+
+ def __str__(self) -> str:
+ return self.isoformat("T")
+
+ def __repr__(self) -> str:
+ us = ""
+ if self.microsecond:
+ us = f", {self.microsecond}"
+
+ repr_ = "{klass}(" "{year}, {month}, {day}, " "{hour}, {minute}, {second}{us}"
+
+ if self.tzinfo is not None:
+ repr_ += ", tzinfo={tzinfo}"
+
+ repr_ += ")"
+
+ return repr_.format(
+ klass=self.__class__.__name__,
+ year=self.year,
+ month=self.month,
+ day=self.day,
+ hour=self.hour,
+ minute=self.minute,
+ second=self.second,
+ us=us,
+ tzinfo=repr(self.tzinfo),
+ )
+
+ # Comparisons
+ def closest(self, *dts: datetime.datetime) -> DateTime: # type: ignore[override]
+ """
+ Get the farthest date from the instance.
+ """
+ pdts = [pendulum.instance(x) for x in dts]
+
+ return min((abs(self - dt), dt) for dt in pdts)[1]
+
+ def farthest(self, *dts: datetime.datetime) -> DateTime: # type: ignore[override]
+ """
+ Get the farthest date from the instance.
+ """
+ pdts = [pendulum.instance(x) for x in dts]
+
+ return max((abs(self - dt), dt) for dt in pdts)[1]
+
+ def is_future(self) -> bool:
+ """
+ Determines if the instance is in the future, ie. greater than now.
+ """
+ return self > self.now(self.timezone)
+
+ def is_past(self) -> bool:
+ """
+ Determines if the instance is in the past, ie. less than now.
+ """
+ return self < self.now(self.timezone)
+
+ def is_long_year(self) -> bool:
+ """
+ Determines if the instance is a long year
+
+ See link `https://en.wikipedia.org/wiki/ISO_8601#Week_dates`_
+ """
+ return (
+ DateTime.create(self.year, 12, 28, 0, 0, 0, tz=self.tz).isocalendar()[1]
+ == 53
+ )
+
+ def is_same_day(self, dt: datetime.datetime) -> bool: # type: ignore[override]
+ """
+ Checks if the passed in date is the same day
+ as the instance current day.
+ """
+ dt = pendulum.instance(dt)
+
+ return self.to_date_string() == dt.to_date_string()
+
+ def is_anniversary( # type: ignore[override]
+ self, dt: datetime.datetime | None = None
+ ) -> bool:
+ """
+ Check if its the anniversary.
+ Compares the date/month values of the two dates.
+ """
+ if dt is None:
+ dt = self.now(self.tz)
+
+ instance = pendulum.instance(dt)
+
+ return (self.month, self.day) == (instance.month, instance.day)
+
+ # ADDITIONS AND SUBSTRACTIONS
+
+ def add(
+ self,
+ years: int = 0,
+ months: int = 0,
+ weeks: int = 0,
+ days: int = 0,
+ hours: int = 0,
+ minutes: int = 0,
+ seconds: float = 0,
+ microseconds: int = 0,
+ ) -> DateTime:
+ """
+ Add a duration to the instance.
+
+ If we're adding units of variable length (i.e., years, months),
+ move forward from current time, otherwise move forward from utc, for accuracy
+ when moving across DST boundaries.
+ """
+ units_of_variable_length = any([years, months, weeks, days])
+
+ current_dt = datetime.datetime(
+ self.year,
+ self.month,
+ self.day,
+ self.hour,
+ self.minute,
+ self.second,
+ self.microsecond,
+ )
+ if not units_of_variable_length:
+ offset = self.utcoffset()
+ if offset:
+ current_dt = current_dt - offset
+
+ dt = add_duration(
+ current_dt,
+ years=years,
+ months=months,
+ weeks=weeks,
+ days=days,
+ hours=hours,
+ minutes=minutes,
+ seconds=seconds,
+ microseconds=microseconds,
+ )
+
+ if units_of_variable_length or self.tz is None:
+ return DateTime.create(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ tz=self.tz,
+ )
+
+ dt = datetime.datetime(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ tzinfo=UTC,
+ )
+
+ dt = self.tz.convert(dt)
+
+ return self.__class__(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ tzinfo=self.tz,
+ fold=dt.fold,
+ )
+
+ def subtract(
+ self,
+ years: int = 0,
+ months: int = 0,
+ weeks: int = 0,
+ days: int = 0,
+ hours: int = 0,
+ minutes: int = 0,
+ seconds: float = 0,
+ microseconds: int = 0,
+ ) -> DateTime:
+ """
+ Remove duration from the instance.
+ """
+ return self.add(
+ years=-years,
+ months=-months,
+ weeks=-weeks,
+ days=-days,
+ hours=-hours,
+ minutes=-minutes,
+ seconds=-seconds,
+ microseconds=-microseconds,
+ )
+
+ # Adding a final underscore to the method name
+ # to avoid errors for PyPy which already defines
+ # a _add_timedelta method
+ def _add_timedelta_(self, delta: datetime.timedelta) -> DateTime:
+ """
+ Add timedelta duration to the instance.
+ """
+ if isinstance(delta, pendulum.Interval):
+ return self.add(
+ years=delta.years,
+ months=delta.months,
+ weeks=delta.weeks,
+ days=delta.remaining_days,
+ hours=delta.hours,
+ minutes=delta.minutes,
+ seconds=delta.remaining_seconds,
+ microseconds=delta.microseconds,
+ )
+ elif isinstance(delta, pendulum.Duration):
+ return self.add(
+ years=delta.years, months=delta.months, seconds=delta._total
+ )
+
+ return self.add(seconds=delta.total_seconds())
+
+ def _subtract_timedelta(self, delta: datetime.timedelta) -> DateTime:
+ """
+ Remove timedelta duration from the instance.
+ """
+ if isinstance(delta, pendulum.Duration):
+ return self.subtract(
+ years=delta.years, months=delta.months, seconds=delta._total
+ )
+
+ return self.subtract(seconds=delta.total_seconds())
+
+ # DIFFERENCES
+
+ def diff( # type: ignore[override]
+ self, dt: datetime.datetime | None = None, abs: bool = True
+ ) -> Interval:
+ """
+ Returns the difference between two DateTime objects represented as an Interval.
+ """
+ if dt is None:
+ dt = self.now(self.tz)
+
+ return Interval(self, dt, absolute=abs)
+
+ def diff_for_humans( # type: ignore[override]
+ self,
+ other: DateTime | None = None,
+ absolute: bool = False,
+ locale: str | None = None,
+ ) -> str:
+ """
+ Get the difference in a human readable format in the current locale.
+
+ When comparing a value in the past to default now:
+ 1 day ago
+ 5 months ago
+
+ When comparing a value in the future to default now:
+ 1 day from now
+ 5 months from now
+
+ When comparing a value in the past to another value:
+ 1 day before
+ 5 months before
+
+ When comparing a value in the future to another value:
+ 1 day after
+ 5 months after
+ """
+ is_now = other is None
+
+ if is_now:
+ other = self.now()
+
+ diff = self.diff(other)
+
+ return pendulum.format_diff(diff, is_now, absolute, locale)
+
+ # Modifiers
+ def start_of(self, unit: str) -> DateTime:
+ """
+ Returns a copy of the instance with the time reset
+ with the following rules:
+
+ * second: microsecond set to 0
+ * minute: second and microsecond set to 0
+ * hour: minute, second and microsecond set to 0
+ * day: time to 00:00:00
+ * week: date to first day of the week and time to 00:00:00
+ * month: date to first day of the month and time to 00:00:00
+ * year: date to first day of the year and time to 00:00:00
+ * decade: date to first day of the decade and time to 00:00:00
+ * century: date to first day of century and time to 00:00:00
+ """
+ if unit not in self._MODIFIERS_VALID_UNITS:
+ raise ValueError(f'Invalid unit "{unit}" for start_of()')
+
+ return cast(DateTime, getattr(self, f"_start_of_{unit}")())
+
+ def end_of(self, unit: str) -> DateTime:
+ """
+ Returns a copy of the instance with the time reset
+ with the following rules:
+
+ * second: microsecond set to 999999
+ * minute: second set to 59 and microsecond set to 999999
+ * hour: minute and second set to 59 and microsecond set to 999999
+ * day: time to 23:59:59.999999
+ * week: date to last day of the week and time to 23:59:59.999999
+ * month: date to last day of the month and time to 23:59:59.999999
+ * year: date to last day of the year and time to 23:59:59.999999
+ * decade: date to last day of the decade and time to 23:59:59.999999
+ * century: date to last day of century and time to 23:59:59.999999
+ """
+ if unit not in self._MODIFIERS_VALID_UNITS:
+ raise ValueError(f'Invalid unit "{unit}" for end_of()')
+
+ return cast(DateTime, getattr(self, f"_end_of_{unit}")())
+
+ def _start_of_second(self) -> DateTime:
+ """
+ Reset microseconds to 0.
+ """
+ return self.set(microsecond=0)
+
+ def _end_of_second(self) -> DateTime:
+ """
+ Set microseconds to 999999.
+ """
+ return self.set(microsecond=999999)
+
+ def _start_of_minute(self) -> DateTime:
+ """
+ Reset seconds and microseconds to 0.
+ """
+ return self.set(second=0, microsecond=0)
+
+ def _end_of_minute(self) -> DateTime:
+ """
+ Set seconds to 59 and microseconds to 999999.
+ """
+ return self.set(second=59, microsecond=999999)
+
+ def _start_of_hour(self) -> DateTime:
+ """
+ Reset minutes, seconds and microseconds to 0.
+ """
+ return self.set(minute=0, second=0, microsecond=0)
+
+ def _end_of_hour(self) -> DateTime:
+ """
+ Set minutes and seconds to 59 and microseconds to 999999.
+ """
+ return self.set(minute=59, second=59, microsecond=999999)
+
+ def _start_of_day(self) -> DateTime:
+ """
+ Reset the time to 00:00:00.
+ """
+ return self.at(0, 0, 0, 0)
+
+ def _end_of_day(self) -> DateTime:
+ """
+ Reset the time to 23:59:59.999999.
+ """
+ return self.at(23, 59, 59, 999999)
+
+ def _start_of_month(self) -> DateTime:
+ """
+ Reset the date to the first day of the month and the time to 00:00:00.
+ """
+ return self.set(self.year, self.month, 1, 0, 0, 0, 0)
+
+ def _end_of_month(self) -> DateTime:
+ """
+ Reset the date to the last day of the month
+ and the time to 23:59:59.999999.
+ """
+ return self.set(self.year, self.month, self.days_in_month, 23, 59, 59, 999999)
+
+ def _start_of_year(self) -> DateTime:
+ """
+ Reset the date to the first day of the year and the time to 00:00:00.
+ """
+ return self.set(self.year, 1, 1, 0, 0, 0, 0)
+
+ def _end_of_year(self) -> DateTime:
+ """
+ Reset the date to the last day of the year
+ and the time to 23:59:59.999999.
+ """
+ return self.set(self.year, 12, 31, 23, 59, 59, 999999)
+
+ def _start_of_decade(self) -> DateTime:
+ """
+ Reset the date to the first day of the decade
+ and the time to 00:00:00.
+ """
+ year = self.year - self.year % YEARS_PER_DECADE
+ return self.set(year, 1, 1, 0, 0, 0, 0)
+
+ def _end_of_decade(self) -> DateTime:
+ """
+ Reset the date to the last day of the decade
+ and the time to 23:59:59.999999.
+ """
+ year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1
+
+ return self.set(year, 12, 31, 23, 59, 59, 999999)
+
+ def _start_of_century(self) -> DateTime:
+ """
+ Reset the date to the first day of the century
+ and the time to 00:00:00.
+ """
+ year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1
+
+ return self.set(year, 1, 1, 0, 0, 0, 0)
+
+ def _end_of_century(self) -> DateTime:
+ """
+ Reset the date to the last day of the century
+ and the time to 23:59:59.999999.
+ """
+ year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY
+
+ return self.set(year, 12, 31, 23, 59, 59, 999999)
+
+ def _start_of_week(self) -> DateTime:
+ """
+ Reset the date to the first day of the week
+ and the time to 00:00:00.
+ """
+ dt = self
+
+ if self.day_of_week != pendulum._WEEK_STARTS_AT:
+ dt = self.previous(pendulum._WEEK_STARTS_AT)
+
+ return dt.start_of("day")
+
+ def _end_of_week(self) -> DateTime:
+ """
+ Reset the date to the last day of the week
+ and the time to 23:59:59.
+ """
+ dt = self
+
+ if self.day_of_week != pendulum._WEEK_ENDS_AT:
+ dt = self.next(pendulum._WEEK_ENDS_AT)
+
+ return dt.end_of("day")
+
+ def next(self, day_of_week: int | None = None, keep_time: bool = False) -> DateTime:
+ """
+ Modify to the next occurrence of a given day of the week.
+ If no day_of_week is provided, modify to the next occurrence
+ of the current day of the week. Use the supplied consts
+ to indicate the desired day_of_week, ex. DateTime.MONDAY.
+ """
+ if day_of_week is None:
+ day_of_week = self.day_of_week
+
+ if day_of_week < 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: int | None = None, keep_time: bool = False
+ ) -> DateTime:
+ """
+ Modify to the previous occurrence of a given day of the week.
+ If no day_of_week is provided, modify to the previous occurrence
+ of the current day of the week. Use the supplied consts
+ to indicate the desired day_of_week, ex. DateTime.MONDAY.
+ """
+ if day_of_week is None:
+ day_of_week = self.day_of_week
+
+ if day_of_week < 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: str, day_of_week: int | None = None) -> DateTime:
+ """
+ Returns an instance set to the first occurrence
+ of a given day of the week in the current unit.
+ If no day_of_week is provided, modify to the first day of the unit.
+ Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY.
+
+ Supported units are month, quarter and year.
+ """
+ if unit not in ["month", "quarter", "year"]:
+ raise ValueError(f'Invalid unit "{unit}" for first_of()')
+
+ return cast(DateTime, getattr(self, f"_first_of_{unit}")(day_of_week))
+
+ def last_of(self, unit: str, day_of_week: int | None = None) -> DateTime:
+ """
+ Returns an instance set to the last occurrence
+ of a given day of the week in the current unit.
+ If no day_of_week is provided, modify to the last day of the unit.
+ Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY.
+
+ Supported units are month, quarter and year.
+ """
+ if unit not in ["month", "quarter", "year"]:
+ raise ValueError(f'Invalid unit "{unit}" for first_of()')
+
+ return cast(DateTime, getattr(self, f"_last_of_{unit}")(day_of_week))
+
+ def nth_of(self, unit: str, nth: int, day_of_week: int) -> DateTime:
+ """
+ Returns a new instance set to the given occurrence
+ of a given day of the week in the current unit.
+ If the calculated occurrence is outside the scope of the current unit,
+ then raise an error. Use the supplied consts
+ to indicate the desired day_of_week, ex. DateTime.MONDAY.
+
+ Supported units are month, quarter and year.
+ """
+ if unit not in ["month", "quarter", "year"]:
+ raise ValueError(f'Invalid unit "{unit}" for first_of()')
+
+ dt = cast(
+ Optional[DateTime], getattr(self, f"_nth_of_{unit}")(nth, day_of_week)
+ )
+ if not dt:
+ raise PendulumException(
+ f"Unable to find occurence {nth}"
+ f" of {self._days[day_of_week]} in {unit}"
+ )
+
+ return dt
+
+ def _first_of_month(self, day_of_week: int | None = None) -> DateTime:
+ """
+ Modify to the first occurrence of a given day of the week
+ in the current month. If no day_of_week is provided,
+ modify to the first day of the month. Use the supplied consts
+ to indicate the desired day_of_week, ex. DateTime.MONDAY.
+ """
+ dt = self.start_of("day")
+
+ if day_of_week is None:
+ return dt.set(day=1)
+
+ month = calendar.monthcalendar(dt.year, dt.month)
+
+ calendar_day = (day_of_week - 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: int | None = None) -> DateTime:
+ """
+ Modify to the last occurrence of a given day of the week
+ in the current month. If no day_of_week is provided,
+ modify to the last day of the month. Use the supplied consts
+ to indicate the desired day_of_week, ex. DateTime.MONDAY.
+ """
+ dt = self.start_of("day")
+
+ if day_of_week is None:
+ return dt.set(day=self.days_in_month)
+
+ month = calendar.monthcalendar(dt.year, dt.month)
+
+ calendar_day = (day_of_week - 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: int, day_of_week: int | None = None
+ ) -> DateTime | None:
+ """
+ Modify to the given occurrence of a given day of the week
+ in the current month. If the calculated occurrence is outside,
+ the scope of the current month, then return False and no
+ modifications are made. Use the supplied consts
+ to indicate the desired day_of_week, ex. DateTime.MONDAY.
+ """
+ if nth == 1:
+ return self.first_of("month", day_of_week)
+
+ dt = self.first_of("month")
+ check = dt.format("%Y-%M")
+ for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
+ dt = dt.next(day_of_week)
+
+ if dt.format("%Y-%M") == check:
+ return self.set(day=dt.day).start_of("day")
+
+ return None
+
+ def _first_of_quarter(self, day_of_week: int | None = None) -> DateTime:
+ """
+ Modify to the first occurrence of a given day of the week
+ in the current quarter. If no day_of_week is provided,
+ modify to the first day of the quarter. Use the supplied consts
+ to indicate the desired day_of_week, ex. DateTime.MONDAY.
+ """
+ return self.on(self.year, self.quarter * 3 - 2, 1).first_of(
+ "month", day_of_week
+ )
+
+ def _last_of_quarter(self, day_of_week: int | None = None) -> DateTime:
+ """
+ Modify to the last occurrence of a given day of the week
+ in the current quarter. If no day_of_week is provided,
+ modify to the last day of the quarter. Use the supplied consts
+ to indicate the desired day_of_week, ex. DateTime.MONDAY.
+ """
+ return self.on(self.year, self.quarter * 3, 1).last_of("month", day_of_week)
+
+ def _nth_of_quarter(
+ self, nth: int, day_of_week: int | None = None
+ ) -> DateTime | None:
+ """
+ Modify to the given occurrence of a given day of the week
+ in the current quarter. If the calculated occurrence is outside,
+ the scope of the current quarter, then return False and no
+ modifications are made. Use the supplied consts
+ to indicate the desired day_of_week, ex. DateTime.MONDAY.
+ """
+ if nth == 1:
+ return self.first_of("quarter", day_of_week)
+
+ dt = self.set(day=1, month=self.quarter * 3)
+ last_month = dt.month
+ year = dt.year
+ dt = dt.first_of("quarter")
+ for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
+ dt = dt.next(day_of_week)
+
+ if last_month < dt.month or year != dt.year:
+ return None
+
+ return self.on(self.year, dt.month, dt.day).start_of("day")
+
+ def _first_of_year(self, day_of_week: int | None = None) -> DateTime:
+ """
+ Modify to the first occurrence of a given day of the week
+ in the current year. If no day_of_week is provided,
+ modify to the first day of the year. Use the supplied consts
+ to indicate the desired day_of_week, ex. DateTime.MONDAY.
+ """
+ return self.set(month=1).first_of("month", day_of_week)
+
+ def _last_of_year(self, day_of_week: int | None = None) -> DateTime:
+ """
+ Modify to the last occurrence of a given day of the week
+ in the current year. If no day_of_week is provided,
+ modify to the last day of the year. Use the supplied consts
+ to indicate the desired day_of_week, ex. DateTime.MONDAY.
+ """
+ return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week)
+
+ def _nth_of_year(self, nth: int, day_of_week: int | None = None) -> DateTime | None:
+ """
+ Modify to the given occurrence of a given day of the week
+ in the current year. If the calculated occurrence is outside,
+ the scope of the current year, then return False and no
+ modifications are made. Use the supplied consts
+ to indicate the desired day_of_week, ex. DateTime.MONDAY.
+ """
+ if nth == 1:
+ return self.first_of("year", day_of_week)
+
+ dt = self.first_of("year")
+ year = dt.year
+ for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
+ dt = dt.next(day_of_week)
+
+ if year != dt.year:
+ return None
+
+ return self.on(self.year, dt.month, dt.day).start_of("day")
+
+ def average( # type: ignore[override]
+ self, dt: datetime.datetime | None = None
+ ) -> DateTime:
+ """
+ Modify the current instance to the average
+ of a given instance (default now) and the current instance.
+ """
+ if dt is None:
+ dt = self.now(self.tz)
+
+ diff = self.diff(dt, False)
+ return self.add(
+ microseconds=(diff.in_seconds() * 1000000 + diff.microseconds) // 2
+ )
+
+ @overload # type: ignore[override]
+ def __sub__(self, other: datetime.timedelta) -> DateTime:
+ ...
+
+ @overload
+ def __sub__(self, other: DateTime) -> Interval:
+ ...
+
+ def __sub__(
+ self, other: datetime.datetime | datetime.timedelta
+ ) -> DateTime | Interval:
+ if isinstance(other, datetime.timedelta):
+ return self._subtract_timedelta(other)
+
+ if not isinstance(other, datetime.datetime):
+ return NotImplemented
+
+ if not isinstance(other, self.__class__):
+ if other.tzinfo is None:
+ other = pendulum.naive(
+ other.year,
+ other.month,
+ other.day,
+ other.hour,
+ other.minute,
+ other.second,
+ other.microsecond,
+ )
+ else:
+ other = pendulum.instance(other)
+
+ return other.diff(self, False)
+
+ def __rsub__(self, other: datetime.datetime) -> Interval:
+ if not isinstance(other, datetime.datetime):
+ return NotImplemented
+
+ if not isinstance(other, self.__class__):
+ if other.tzinfo is None:
+ other = pendulum.naive(
+ other.year,
+ other.month,
+ other.day,
+ other.hour,
+ other.minute,
+ other.second,
+ other.microsecond,
+ )
+ else:
+ other = pendulum.instance(other)
+
+ return self.diff(other, False)
+
+ def __add__(self, other: datetime.timedelta) -> DateTime:
+ if not isinstance(other, datetime.timedelta):
+ return NotImplemented
+
+ if PY38:
+ # This is a workaround for Python 3.8+
+ # since calling astimezone() will call this method
+ # instead of the base datetime class one.
+ import inspect
+
+ caller = inspect.stack()[1][3]
+ if caller == "astimezone":
+ return cast(DateTime, super().__add__(other))
+
+ return self._add_timedelta_(other)
+
+ def __radd__(self, other: datetime.timedelta) -> DateTime:
+ return self.__add__(other)
+
+ # Native methods override
+
+ @classmethod
+ def fromtimestamp(cls, t: float, tz: datetime.tzinfo | None = None) -> DateTime:
+ tzinfo = pendulum._safe_timezone(tz)
+
+ return pendulum.instance(
+ datetime.datetime.fromtimestamp(t, tz=tzinfo), tz=tzinfo
+ )
+
+ @classmethod
+ def utcfromtimestamp(cls, t: float) -> DateTime:
+ return pendulum.instance(datetime.datetime.utcfromtimestamp(t), tz=None)
+
+ @classmethod
+ def fromordinal(cls, n: int) -> DateTime:
+ return pendulum.instance(datetime.datetime.fromordinal(n), tz=None)
+
+ @classmethod
+ def combine(
+ cls,
+ date: datetime.date,
+ time: datetime.time,
+ tzinfo: datetime.tzinfo | None = None,
+ ) -> DateTime:
+ return pendulum.instance(datetime.datetime.combine(date, time), tz=tzinfo)
+
+ def astimezone(self, tz: datetime.tzinfo | None = None) -> DateTime:
+ dt = super().astimezone(tz)
+
+ return self.__class__(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ fold=dt.fold,
+ tzinfo=dt.tzinfo,
+ )
+
+ def replace(
+ self,
+ year: int | None = None,
+ month: int | None = None,
+ day: int | None = None,
+ hour: int | None = None,
+ minute: int | None = None,
+ second: int | None = None,
+ microsecond: int | None = None,
+ tzinfo: bool | datetime.tzinfo | Literal[True] | None = True,
+ fold: int | None = None,
+ ) -> DateTime:
+ if year is None:
+ year = self.year
+ if month is None:
+ month = self.month
+ if day is None:
+ day = self.day
+ if hour is None:
+ hour = self.hour
+ if minute is None:
+ minute = self.minute
+ if second is None:
+ second = self.second
+ if microsecond is None:
+ microsecond = self.microsecond
+ if tzinfo is True:
+ tzinfo = self.tzinfo
+ if fold is None:
+ fold = self.fold
+
+ if tzinfo is not None:
+ tzinfo = pendulum._safe_timezone(tzinfo)
+
+ return DateTime.create(
+ year,
+ month,
+ day,
+ hour,
+ minute,
+ second,
+ microsecond,
+ tz=tzinfo,
+ fold=fold,
+ )
+
+ def __getnewargs__(self) -> tuple[DateTime]:
+ return (self,)
+
+ def _getstate(
+ self, protocol: int = 3
+ ) -> tuple[int, int, int, int, int, int, int, datetime.tzinfo | None]:
+ return (
+ self.year,
+ self.month,
+ self.day,
+ self.hour,
+ self.minute,
+ self.second,
+ self.microsecond,
+ self.tzinfo,
+ )
+
+ def __reduce__(
+ self,
+ ) -> tuple[
+ type[DateTime], tuple[int, int, int, int, int, int, int, datetime.tzinfo | None]
+ ]:
+ return self.__reduce_ex__(2)
+
+ def __reduce_ex__( # type: ignore[override]
+ self, protocol: int
+ ) -> tuple[
+ type[DateTime], tuple[int, int, int, int, int, int, int, datetime.tzinfo | None]
+ ]:
+ return self.__class__, self._getstate(protocol)
+
+ def _cmp(self, other: datetime.datetime, **kwargs: Any) -> int:
+ # Fix for pypy which compares using this method
+ # which would lead to infinite recursion if we didn't override
+ dt = datetime.datetime(
+ self.year,
+ self.month,
+ self.day,
+ self.hour,
+ self.minute,
+ self.second,
+ self.microsecond,
+ tzinfo=self.tz,
+ fold=self.fold,
+ )
+
+ return 0 if dt == other else 1 if dt > other else -1
+
+
+DateTime.min: DateTime = DateTime(1, 1, 1, 0, 0, tzinfo=UTC) # type: ignore[misc]
+DateTime.max: DateTime = DateTime( # type: ignore[misc]
+ 9999, 12, 31, 23, 59, 59, 999999, tzinfo=UTC
+)
+DateTime.EPOCH: DateTime = DateTime(1970, 1, 1, tzinfo=UTC) # type: ignore[misc]
diff --git a/pendulum/duration.py b/pendulum/duration.py
index 18d0c7f..a3a68b1 100644
--- a/pendulum/duration.py
+++ b/pendulum/duration.py
@@ -1,479 +1,502 @@
-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
+from __future__ import annotations
+
+from datetime import timedelta
+from typing import cast
+from typing import overload
+
+import pendulum
+
+from pendulum.constants import SECONDS_PER_DAY
+from pendulum.constants import SECONDS_PER_HOUR
+from pendulum.constants import SECONDS_PER_MINUTE
+from pendulum.constants import US_PER_SECOND
+from pendulum.utils._compat import PYPY
+
+
+def _divide_and_round(a: float, b: float) -> int:
+ """divide a by b and round result to the nearest integer
+
+ When the ratio is exactly half-way between two integers,
+ the even integer is returned.
+ """
+ # Based on the reference implementation for divmod_near
+ # in Objects/longobject.c.
+ q, r = divmod(a, b)
+
+ # The output of divmod() is either a float or an int,
+ # but we always want it to be an int.
+ q = int(q)
+
+ # round up if either r / b > 0.5, or r / b == 0.5 and q is odd.
+ # The expression r / b > 0.5 is equivalent to 2 * r > b if b is
+ # positive, 2 * r < b if b negative.
+ r *= 2
+ greater_than_half = r > b if b > 0 else r < b
+ if greater_than_half or r == b and q % 2 == 1:
+ q += 1
+
+ return q
+
+
+class Duration(timedelta):
+ """
+ Replacement for the standard timedelta class.
+
+ Provides several improvements over the base class.
+ """
+
+ _total: float = 0
+ _years: int = 0
+ _months: int = 0
+ _weeks: int = 0
+ _days: int = 0
+ _remaining_days: int = 0
+ _seconds: int = 0
+ _microseconds: int = 0
+
+ _y = None
+ _m = None
+ _w = None
+ _d = None
+ _h = None
+ _i = None
+ _s = None
+ _invert = None
+
+ def __new__(
+ cls,
+ days: float = 0,
+ seconds: float = 0,
+ microseconds: float = 0,
+ milliseconds: float = 0,
+ minutes: float = 0,
+ hours: float = 0,
+ weeks: float = 0,
+ years: float = 0,
+ months: float = 0,
+ ) -> Duration:
+ 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) -> float:
+ return self.total_seconds() / SECONDS_PER_MINUTE
+
+ def total_hours(self) -> float:
+ return self.total_seconds() / SECONDS_PER_HOUR
+
+ def total_days(self) -> float:
+ return self.total_seconds() / SECONDS_PER_DAY
+
+ def total_weeks(self) -> float:
+ return self.total_days() / 7
+
+ if PYPY:
+
+ def total_seconds(self) -> float:
+ days = 0
+
+ if hasattr(self, "_years"):
+ days += self._years * 365
+
+ if hasattr(self, "_months"):
+ days += self._months * 30
+
+ if hasattr(self, "_remaining_days"):
+ days += self._weeks * 7 + self._remaining_days
+ else:
+ days += self._days
+
+ return (
+ (days * SECONDS_PER_DAY + self._seconds) * US_PER_SECOND
+ + self._microseconds
+ ) / US_PER_SECOND
+
+ @property
+ def years(self) -> int:
+ return self._years
+
+ @property
+ def months(self) -> int:
+ return self._months
+
+ @property
+ def weeks(self) -> int:
+ return self._weeks
+
+ if PYPY:
+
+ @property
+ def days(self) -> int:
+ return self._years * 365 + self._months * 30 + self._days
+
+ @property
+ def remaining_days(self) -> int:
+ return self._remaining_days
+
+ @property
+ def hours(self) -> int:
+ if self._h is None:
+ seconds = self._seconds
+ self._h = 0
+ if abs(seconds) >= 3600:
+ self._h = (abs(seconds) // 3600 % 24) * self._sign(seconds)
+
+ return self._h
+
+ @property
+ def minutes(self) -> int:
+ if self._i is None:
+ seconds = self._seconds
+ self._i = 0
+ if abs(seconds) >= 60:
+ self._i = (abs(seconds) // 60 % 60) * self._sign(seconds)
+
+ return self._i
+
+ @property
+ def seconds(self) -> int:
+ return self._seconds
+
+ @property
+ def remaining_seconds(self) -> int:
+ if self._s is None:
+ self._s = self._seconds
+ self._s = abs(self._s) % 60 * self._sign(self._s)
+
+ return self._s
+
+ @property
+ def microseconds(self) -> int:
+ return self._microseconds
+
+ @property
+ def invert(self) -> bool:
+ if self._invert is None:
+ self._invert = self.total_seconds() < 0
+
+ return self._invert
+
+ def in_weeks(self) -> int:
+ return int(self.total_weeks())
+
+ def in_days(self) -> int:
+ return int(self.total_days())
+
+ def in_hours(self) -> int:
+ return int(self.total_hours())
+
+ def in_minutes(self) -> int:
+ return int(self.total_minutes())
+
+ def in_seconds(self) -> int:
+ return int(self.total_seconds())
+
+ def in_words(self, locale: str | None = None, separator: str = " ") -> str:
+ """
+ Get the current interval in words in the current locale.
+
+ Ex: 6 jours 23 heures 58 minutes
+
+ :param locale: The locale to use. Defaults to current locale.
+ :param separator: The separator to use between each unit
+ """
+ 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()
+
+ loaded_locale = pendulum.locale(locale)
+
+ parts = []
+ for period in periods:
+ unit, period_count = period
+ if abs(period_count) > 0:
+ translation = loaded_locale.translation(
+ f"units.{unit}.{loaded_locale.plural(abs(period_count))}"
+ )
+ parts.append(translation.format(period_count))
+
+ if not parts:
+ count: int | str = 0
+ if abs(self.microseconds) > 0:
+ unit = f"units.second.{loaded_locale.plural(1)}"
+ count = f"{abs(self.microseconds) / 1e6:.2f}"
+ else:
+ unit = f"units.microsecond.{loaded_locale.plural(0)}"
+ translation = loaded_locale.translation(unit)
+ parts.append(translation.format(count))
+
+ return separator.join(parts)
+
+ def _sign(self, value: float) -> int:
+ if value < 0:
+ return -1
+
+ return 1
+
+ def as_timedelta(self) -> timedelta:
+ """
+ Return the interval as a native timedelta.
+ """
+ return timedelta(seconds=self.total_seconds())
+
+ def __str__(self) -> str:
+ return self.in_words()
+
+ def __repr__(self) -> str:
+ rep = f"{self.__class__.__name__}("
+
+ if self._years:
+ rep += f"years={self._years}, "
+
+ if self._months:
+ rep += f"months={self._months}, "
+
+ if self._weeks:
+ rep += f"weeks={self._weeks}, "
+
+ if self._days:
+ rep += f"days={self._remaining_days}, "
+
+ if self.hours:
+ rep += f"hours={self.hours}, "
+
+ if self.minutes:
+ rep += f"minutes={self.minutes}, "
+
+ if self.remaining_seconds:
+ rep += f"seconds={self.remaining_seconds}, "
+
+ if self.microseconds:
+ rep += f"microseconds={self.microseconds}, "
+
+ rep += ")"
+
+ return rep.replace(", )", ")")
+
+ def __add__(self, other: timedelta) -> Duration:
+ if isinstance(other, timedelta):
+ return self.__class__(seconds=self.total_seconds() + other.total_seconds())
+
+ return NotImplemented
+
+ __radd__ = __add__
+
+ def __sub__(self, other: timedelta) -> Duration:
+ if isinstance(other, timedelta):
+ return self.__class__(seconds=self.total_seconds() - other.total_seconds())
+
+ return NotImplemented
+
+ def __neg__(self) -> Duration:
+ return self.__class__(
+ years=-self._years,
+ months=-self._months,
+ weeks=-self._weeks,
+ days=-self._remaining_days,
+ seconds=-self._seconds,
+ microseconds=-self._microseconds,
+ )
+
+ def _to_microseconds(self) -> int:
+ return (self._days * (24 * 3600) + self._seconds) * 1000000 + self._microseconds
+
+ def __mul__(self, other: int | float) -> Duration:
+ if isinstance(other, int):
+ return self.__class__(
+ years=self._years * other,
+ months=self._months * other,
+ seconds=self._total * other,
+ )
+
+ if isinstance(other, float):
+ usec = self._to_microseconds()
+ a, b = other.as_integer_ratio()
+
+ return self.__class__(0, 0, _divide_and_round(usec * a, b))
+
+ return NotImplemented
+
+ __rmul__ = __mul__
+
+ @overload
+ def __floordiv__(self, other: timedelta) -> int:
+ ...
+
+ @overload
+ def __floordiv__(self, other: int) -> Duration:
+ ...
+
+ def __floordiv__(self, other: int | timedelta) -> int | Duration:
+ if not isinstance(other, (int, timedelta)):
+ return NotImplemented
+
+ usec = self._to_microseconds()
+ if isinstance(other, timedelta):
+ return cast(int, usec // other._to_microseconds()) # type: ignore[attr-defined]
+
+ if isinstance(other, int):
+ return self.__class__(
+ 0,
+ 0,
+ usec // other,
+ years=self._years // other,
+ months=self._months // other,
+ )
+
+ @overload
+ def __truediv__(self, other: timedelta) -> float:
+ ...
+
+ @overload
+ def __truediv__(self, other: float) -> Duration:
+ ...
+
+ def __truediv__(self, other: int | float | timedelta) -> Duration | float:
+ if not isinstance(other, (int, float, timedelta)):
+ return NotImplemented
+
+ usec = self._to_microseconds()
+ if isinstance(other, timedelta):
+ return cast(float, usec / other._to_microseconds()) # type: ignore[attr-defined]
+
+ if isinstance(other, int):
+ return self.__class__(
+ 0,
+ 0,
+ _divide_and_round(usec, other),
+ years=_divide_and_round(self._years, other),
+ months=_divide_and_round(self._months, other),
+ )
+
+ if isinstance(other, float):
+ a, b = other.as_integer_ratio()
+
+ return self.__class__(
+ 0,
+ 0,
+ _divide_and_round(b * usec, a),
+ years=_divide_and_round(self._years * b, a),
+ months=_divide_and_round(self._months, other),
+ )
+
+ __div__ = __floordiv__
+
+ def __mod__(self, other: timedelta) -> Duration:
+ if isinstance(other, timedelta):
+ r = self._to_microseconds() % other._to_microseconds() # type: ignore[attr-defined]
+
+ return self.__class__(0, 0, r)
+
+ return NotImplemented
+
+ def __divmod__(self, other: timedelta) -> tuple[int, Duration]:
+ if isinstance(other, timedelta):
+ q, r = divmod(self._to_microseconds(), other._to_microseconds()) # type: ignore[attr-defined]
+
+ return q, self.__class__(0, 0, r)
+
+ return NotImplemented
+
+
+Duration.min = Duration(days=-999999999)
+Duration.max = Duration(
+ days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999
+)
+Duration.resolution = Duration(microseconds=1)
+
+
+class AbsoluteDuration(Duration):
+ """
+ Duration that expresses a time difference in absolute values.
+ """
+
+ def __new__(
+ cls,
+ days: float = 0,
+ seconds: float = 0,
+ microseconds: float = 0,
+ milliseconds: float = 0,
+ minutes: float = 0,
+ hours: float = 0,
+ weeks: float = 0,
+ years: float = 0,
+ months: float = 0,
+ ) -> AbsoluteDuration:
+ if not isinstance(years, int) or not isinstance(months, int):
+ raise ValueError("Float year and months are not supported")
+
+ self = timedelta.__new__(
+ cls, days, seconds, microseconds, milliseconds, minutes, hours, weeks
+ )
+
+ # We need to compute the total_seconds() value
+ # on a native timedelta object
+ delta = timedelta(
+ days, seconds, microseconds, milliseconds, minutes, hours, weeks
+ )
+
+ # Intuitive normalization
+ self._total = delta.total_seconds()
+ total = abs(self._total)
+
+ self._microseconds = round(total % 1 * 1e6)
+ 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) -> float:
+ return abs(self._total)
+
+ @property
+ def invert(self) -> bool:
+ if self._invert is None:
+ self._invert = self._total < 0
+
+ return self._invert
diff --git a/pendulum/exceptions.py b/pendulum/exceptions.py
index 6806783..3ab4db9 100644
--- a/pendulum/exceptions.py
+++ b/pendulum/exceptions.py
@@ -1,6 +1,8 @@
-from .parsing.exceptions import ParserError # noqa
-
-
-class PendulumException(Exception):
-
- pass
+from __future__ import annotations
+
+from .parsing.exceptions import ParserError # noqa
+
+
+class PendulumException(Exception):
+
+ pass
diff --git a/pendulum/formatting/__init__.py b/pendulum/formatting/__init__.py
index a2b47de..975c409 100644
--- a/pendulum/formatting/__init__.py
+++ b/pendulum/formatting/__init__.py
@@ -1,4 +1,5 @@
-from .formatter import Formatter
-
-
-__all__ = ["Formatter"]
+from __future__ import annotations
+
+from pendulum.formatting.formatter import Formatter
+
+__all__ = ["Formatter"]
diff --git a/pendulum/formatting/difference_formatter.py b/pendulum/formatting/difference_formatter.py
index 3243089..dad219d 100644
--- a/pendulum/formatting/difference_formatter.py
+++ b/pendulum/formatting/difference_formatter.py
@@ -1,153 +1,146 @@
-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))
+from __future__ import annotations
+
+import typing as t
+
+from pendulum.locales.locale import Locale
+
+if t.TYPE_CHECKING:
+ from pendulum import Duration
+
+
+class DifferenceFormatter:
+ """
+ Handles formatting differences in text.
+ """
+
+ def __init__(self, locale: str = "en") -> None:
+ self._locale = Locale.load(locale)
+
+ def format(
+ self,
+ diff: Duration,
+ is_now: bool = True,
+ absolute: bool = False,
+ locale: str | Locale | None = None,
+ ) -> str:
+ """
+ Formats a difference.
+
+ :param diff: The difference to format
+ :param is_now: Whether the difference includes now
+ :param absolute: Whether it's an absolute difference or not
+ :param locale: The locale to use
+ """
+ if locale is None:
+ locale = self._locale
+ else:
+ locale = Locale.load(locale)
+
+ if diff.years > 0:
+ unit = "year"
+ count = diff.years
+
+ if diff.months > 6:
+ count += 1
+ elif diff.months == 11 and (diff.weeks * 7 + diff.remaining_days) > 15:
+ unit = "year"
+ count = 1
+ elif diff.months > 0:
+ unit = "month"
+ count = diff.months
+
+ if (diff.weeks * 7 + diff.remaining_days) >= 27:
+ count += 1
+ elif diff.weeks > 0:
+ unit = "week"
+ count = diff.weeks
+
+ if diff.remaining_days > 3:
+ count += 1
+ elif diff.remaining_days > 0:
+ unit = "day"
+ count = diff.remaining_days
+
+ if diff.hours >= 22:
+ count += 1
+ elif diff.hours > 0:
+ unit = "hour"
+ count = diff.hours
+ elif diff.minutes > 0:
+ unit = "minute"
+ count = diff.minutes
+ elif 10 < diff.remaining_seconds <= 59:
+ unit = "second"
+ count = diff.remaining_seconds
+ else:
+ # We check if the "a few seconds" unit exists
+ time = locale.get("custom.units.few_second")
+ if time is not None:
+ if absolute:
+ return t.cast(str, time)
+
+ key = "custom"
+ is_future = diff.invert
+ if is_now:
+ if is_future:
+ key += ".from_now"
+ else:
+ key += ".ago"
+ else:
+ if is_future:
+ key += ".after"
+ else:
+ key += ".before"
+
+ return t.cast(str, locale.get(key).format(time))
+ else:
+ unit = "second"
+ count = diff.remaining_seconds
+
+ if count == 0:
+ count = 1
+
+ if absolute:
+ key = f"translations.units.{unit}"
+ else:
+ is_future = diff.invert
+
+ if is_now:
+ # Relative to now, so we can use
+ # the CLDR data
+ key = f"translations.relative.{unit}"
+
+ if is_future:
+ key += ".future"
+ else:
+ key += ".past"
+ else:
+ # Absolute comparison
+ # So we have to use the custom locale data
+
+ # Checking for special pluralization rules
+ key = "custom.units_relative"
+ if is_future:
+ key += f".{unit}.future"
+ else:
+ key += f".{unit}.past"
+
+ trans = locale.get(key)
+ if not trans:
+ # No special rule
+ key = f"translations.units.{unit}.{locale.plural(count)}"
+ time = locale.get(key).format(count)
+ else:
+ time = trans[locale.plural(count)].format(count)
+
+ key = "custom"
+ if is_future:
+ key += ".after"
+ else:
+ key += ".before"
+
+ return t.cast(str, locale.get(key).format(time))
+
+ key += f".{locale.plural(count)}"
+
+ return t.cast(str, locale.get(key).format(count))
diff --git a/pendulum/formatting/formatter.py b/pendulum/formatting/formatter.py
index 4e493d0..f91d5d9 100644
--- a/pendulum/formatting/formatter.py
+++ b/pendulum/formatting/formatter.py
@@ -1,685 +1,683 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-import datetime
-import re
-import typing
-
-import pendulum
-
-from pendulum.locales.locale import Locale
-from pendulum.utils._compat import decode
-
-
-_MATCH_1 = r"\d"
-_MATCH_2 = r"\d\d"
-_MATCH_3 = r"\d{3}"
-_MATCH_4 = r"\d{4}"
-_MATCH_6 = r"[+-]?\d{6}"
-_MATCH_1_TO_2 = r"\d\d?"
-_MATCH_1_TO_2_LEFT_PAD = r"[0-9 ]\d?"
-_MATCH_1_TO_3 = r"\d{1,3}"
-_MATCH_1_TO_4 = r"\d{1,4}"
-_MATCH_1_TO_6 = r"[+-]?\d{1,6}"
-_MATCH_3_TO_4 = r"\d{3}\d?"
-_MATCH_5_TO_6 = r"\d{5}\d?"
-_MATCH_UNSIGNED = r"\d+"
-_MATCH_SIGNED = r"[+-]?\d+"
-_MATCH_OFFSET = r"[Zz]|[+-]\d\d:?\d\d"
-_MATCH_SHORT_OFFSET = r"[Zz]|[+-]\d\d(?::?\d\d)?"
-_MATCH_TIMESTAMP = r"[+-]?\d+(\.\d{1,6})?"
-_MATCH_WORD = (
- "(?i)[0-9]*"
- "['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+"
- r"|[\u0600-\u06FF/]+(\s*?[\u0600-\u06FF]+){1,2}"
-)
-_MATCH_TIMEZONE = "[A-Za-z0-9-+]+(/[A-Za-z0-9-+_]+)?"
-
-
-class Formatter:
-
- _TOKENS = (
- r"\[([^\[]*)\]|\\(.)|"
- "("
- "Mo|MM?M?M?"
- "|Do|DDDo|DD?D?D?|ddd?d?|do?"
- "|E{1,4}"
- "|w[o|w]?|W[o|W]?|Qo?"
- "|YYYY|YY|Y"
- "|gg(ggg?)?|GG(GGG?)?"
- "|a|A"
- "|hh?|HH?|kk?"
- "|mm?|ss?|S{1,9}"
- "|x|X"
- "|zz?|ZZ?"
- "|LTS|LT|LL?L?L?"
- ")"
- )
-
- _FORMAT_RE = re.compile(_TOKENS)
-
- _FROM_FORMAT_RE = re.compile(r"(?<!\\\[)" + _TOKENS + r"(?!\\\])")
-
- _LOCALIZABLE_TOKENS = {
- "Qo": None,
- "MMMM": "months.wide",
- "MMM": "months.abbreviated",
- "Mo": None,
- "DDDo": None,
- "Do": lambda locale: tuple(
- r"\d+{}".format(o) for o in locale.get("custom.ordinal").values()
- ),
- "dddd": "days.wide",
- "ddd": "days.abbreviated",
- "dd": "days.short",
- "do": None,
- "Wo": None,
- "wo": None,
- "A": lambda locale: (
- locale.translation("day_periods.am"),
- locale.translation("day_periods.pm"),
- ),
- "a": lambda locale: (
- locale.translation("day_periods.am").lower(),
- locale.translation("day_periods.pm").lower(),
- ),
- }
-
- _TOKENS_RULES = {
- # Year
- "YYYY": lambda dt: "{:d}".format(dt.year),
- "YY": lambda dt: "{:d}".format(dt.year)[2:],
- "Y": lambda dt: "{:d}".format(dt.year),
- # Quarter
- "Q": lambda dt: "{:d}".format(dt.quarter),
- # Month
- "MM": lambda dt: "{:02d}".format(dt.month),
- "M": lambda dt: "{:d}".format(dt.month),
- # Day
- "DD": lambda dt: "{:02d}".format(dt.day),
- "D": lambda dt: "{:d}".format(dt.day),
- # Day of Year
- "DDDD": lambda dt: "{:03d}".format(dt.day_of_year),
- "DDD": lambda dt: "{:d}".format(dt.day_of_year),
- # Day of Week
- "d": lambda dt: "{:d}".format(dt.day_of_week),
- # Day of ISO Week
- "E": lambda dt: "{:d}".format(dt.isoweekday()),
- # Hour
- "HH": lambda dt: "{:02d}".format(dt.hour),
- "H": lambda dt: "{:d}".format(dt.hour),
- "hh": lambda dt: "{:02d}".format(dt.hour % 12 or 12),
- "h": lambda dt: "{:d}".format(dt.hour % 12 or 12),
- # Minute
- "mm": lambda dt: "{:02d}".format(dt.minute),
- "m": lambda dt: "{:d}".format(dt.minute),
- # Second
- "ss": lambda dt: "{:02d}".format(dt.second),
- "s": lambda dt: "{:d}".format(dt.second),
- # Fractional second
- "S": lambda dt: "{:01d}".format(dt.microsecond // 100000),
- "SS": lambda dt: "{:02d}".format(dt.microsecond // 10000),
- "SSS": lambda dt: "{:03d}".format(dt.microsecond // 1000),
- "SSSS": lambda dt: "{:04d}".format(dt.microsecond // 100),
- "SSSSS": lambda dt: "{:05d}".format(dt.microsecond // 10),
- "SSSSSS": lambda dt: "{:06d}".format(dt.microsecond),
- # Timestamp
- "X": lambda dt: "{:d}".format(dt.int_timestamp),
- "x": lambda dt: "{:d}".format(dt.int_timestamp * 1000 + dt.microsecond // 1000),
- # Timezone
- "zz": lambda dt: "{}".format(dt.tzname() if dt.tzinfo is not None else ""),
- "z": lambda dt: "{}".format(dt.timezone_name or ""),
- }
-
- _DATE_FORMATS = {
- "LTS": "formats.time.full",
- "LT": "formats.time.short",
- "L": "formats.date.short",
- "LL": "formats.date.long",
- "LLL": "formats.datetime.long",
- "LLLL": "formats.datetime.full",
- }
-
- _DEFAULT_DATE_FORMATS = {
- "LTS": "h:mm:ss A",
- "LT": "h:mm A",
- "L": "MM/DD/YYYY",
- "LL": "MMMM D, YYYY",
- "LLL": "MMMM D, YYYY h:mm A",
- "LLLL": "dddd, MMMM D, YYYY h:mm A",
- }
-
- _REGEX_TOKENS = {
- "Y": _MATCH_SIGNED,
- "YY": (_MATCH_1_TO_2, _MATCH_2),
- "YYYY": (_MATCH_1_TO_4, _MATCH_4),
- "Q": _MATCH_1,
- "Qo": None,
- "M": _MATCH_1_TO_2,
- "MM": (_MATCH_1_TO_2, _MATCH_2),
- "MMM": _MATCH_WORD,
- "MMMM": _MATCH_WORD,
- "D": _MATCH_1_TO_2,
- "DD": (_MATCH_1_TO_2_LEFT_PAD, _MATCH_2),
- "DDD": _MATCH_1_TO_3,
- "DDDD": _MATCH_3,
- "dddd": _MATCH_WORD,
- "ddd": _MATCH_WORD,
- "dd": _MATCH_WORD,
- "d": _MATCH_1,
- "E": _MATCH_1,
- "Do": None,
- "H": _MATCH_1_TO_2,
- "HH": (_MATCH_1_TO_2, _MATCH_2),
- "h": _MATCH_1_TO_2,
- "hh": (_MATCH_1_TO_2, _MATCH_2),
- "m": _MATCH_1_TO_2,
- "mm": (_MATCH_1_TO_2, _MATCH_2),
- "s": _MATCH_1_TO_2,
- "ss": (_MATCH_1_TO_2, _MATCH_2),
- "S": (_MATCH_1_TO_3, _MATCH_1),
- "SS": (_MATCH_1_TO_3, _MATCH_2),
- "SSS": (_MATCH_1_TO_3, _MATCH_3),
- "SSSS": _MATCH_UNSIGNED,
- "SSSSS": _MATCH_UNSIGNED,
- "SSSSSS": _MATCH_UNSIGNED,
- "x": _MATCH_SIGNED,
- "X": _MATCH_TIMESTAMP,
- "ZZ": _MATCH_SHORT_OFFSET,
- "Z": _MATCH_OFFSET,
- "z": _MATCH_TIMEZONE,
- }
-
- _PARSE_TOKENS = {
- "YYYY": lambda year: int(year),
- "YY": lambda year: int(year),
- "Q": lambda quarter: int(quarter),
- "MMMM": lambda month: month,
- "MMM": lambda month: month,
- "MM": lambda month: int(month),
- "M": lambda month: int(month),
- "DDDD": lambda day: int(day),
- "DDD": lambda day: int(day),
- "DD": lambda day: int(day),
- "D": lambda day: int(day),
- "dddd": lambda weekday: weekday,
- "ddd": lambda weekday: weekday,
- "dd": lambda weekday: weekday,
- "d": lambda weekday: int(weekday) % 7,
- "E": lambda weekday: int(weekday),
- "HH": lambda hour: int(hour),
- "H": lambda hour: int(hour),
- "hh": lambda hour: int(hour),
- "h": lambda hour: int(hour),
- "mm": lambda minute: int(minute),
- "m": lambda minute: int(minute),
- "ss": lambda second: int(second),
- "s": lambda second: int(second),
- "S": lambda us: int(us) * 100000,
- "SS": lambda us: int(us) * 10000,
- "SSS": lambda us: int(us) * 1000,
- "SSSS": lambda us: int(us) * 100,
- "SSSSS": lambda us: int(us) * 10,
- "SSSSSS": lambda us: int(us),
- "a": lambda meridiem: meridiem,
- "X": lambda ts: float(ts),
- "x": lambda ts: float(ts) / 1e3,
- "ZZ": str,
- "Z": str,
- "z": str,
- }
-
- def format(
- self, dt, fmt, locale=None
- ): # type: (pendulum.DateTime, str, typing.Optional[typing.Union[str, Locale]]) -> str
- """
- Formats a DateTime instance with a given format and locale.
-
- :param dt: The instance to format
- :type dt: pendulum.DateTime
-
- :param fmt: The format to use
- :type fmt: str
-
- :param locale: The locale to use
- :type locale: str or Locale or None
-
- :rtype: str
- """
- if not locale:
- locale = pendulum.get_locale()
-
- locale = Locale.load(locale)
-
- result = self._FORMAT_RE.sub(
- lambda m: m.group(1)
- if m.group(1)
- else m.group(2)
- if m.group(2)
- else self._format_token(dt, m.group(3), locale),
- fmt,
- )
-
- return decode(result)
-
- def _format_token(
- self, dt, token, locale
- ): # type: (pendulum.DateTime, str, Locale) -> str
- """
- Formats a DateTime instance with a given token and locale.
-
- :param dt: The instance to format
- :type dt: pendulum.DateTime
-
- :param token: The token to use
- :type token: str
-
- :param locale: The locale to use
- :type locale: Locale
-
- :rtype: str
- """
- if token in self._DATE_FORMATS:
- fmt = locale.get("custom.date_formats.{}".format(token))
- if fmt is None:
- fmt = self._DEFAULT_DATE_FORMATS[token]
-
- return self.format(dt, fmt, locale)
-
- if token in self._LOCALIZABLE_TOKENS:
- return self._format_localizable_token(dt, token, locale)
-
- if token in self._TOKENS_RULES:
- return self._TOKENS_RULES[token](dt)
-
- # Timezone
- if token in ["ZZ", "Z"]:
- if dt.tzinfo is None:
- return ""
-
- separator = ":" if token == "Z" else ""
- offset = dt.utcoffset() or datetime.timedelta()
- minutes = offset.total_seconds() / 60
-
- if minutes >= 0:
- sign = "+"
- else:
- sign = "-"
-
- hour, minute = divmod(abs(int(minutes)), 60)
-
- return "{}{:02d}{}{:02d}".format(sign, hour, separator, minute)
-
- def _format_localizable_token(
- self, dt, token, locale
- ): # type: (pendulum.DateTime, str, Locale) -> str
- """
- Formats a DateTime instance
- with a given localizable token and locale.
-
- :param dt: The instance to format
- :type dt: pendulum.DateTime
-
- :param token: The token to use
- :type token: str
-
- :param locale: The locale to use
- :type locale: Locale
-
- :rtype: str
- """
- if token == "MMM":
- return locale.get("translations.months.abbreviated")[dt.month]
- elif token == "MMMM":
- return locale.get("translations.months.wide")[dt.month]
- elif token == "dd":
- return locale.get("translations.days.short")[dt.day_of_week]
- elif token == "ddd":
- return locale.get("translations.days.abbreviated")[dt.day_of_week]
- elif token == "dddd":
- return locale.get("translations.days.wide")[dt.day_of_week]
- elif token == "Do":
- return locale.ordinalize(dt.day)
- elif token == "do":
- return locale.ordinalize(dt.day_of_week)
- elif token == "Mo":
- return locale.ordinalize(dt.month)
- elif token == "Qo":
- return locale.ordinalize(dt.quarter)
- elif token == "wo":
- return locale.ordinalize(dt.week_of_year)
- elif token == "DDDo":
- return locale.ordinalize(dt.day_of_year)
- elif token == "A":
- key = "translations.day_periods"
- if dt.hour >= 12:
- key += ".pm"
- else:
- key += ".am"
-
- return locale.get(key)
- else:
- return token
-
- def parse(
- self,
- time, # type: str
- fmt, # type: str
- now, # type: pendulum.DateTime
- locale=None, # type: typing.Optional[str]
- ): # type: (...) -> typing.Dict[str, typing.Any]
- """
- Parses a time string matching a given format as a tuple.
-
- :param time: The timestring
- :param fmt: The format
- :param now: The datetime to use as "now"
- :param locale: The locale to use
-
- :return: The parsed elements
- """
- escaped_fmt = re.escape(fmt)
-
- tokens = self._FROM_FORMAT_RE.findall(escaped_fmt)
- if not tokens:
- return time
-
- if not locale:
- locale = pendulum.get_locale()
-
- locale = Locale.load(locale)
-
- parsed = {
- "year": None,
- "month": None,
- "day": None,
- "hour": None,
- "minute": None,
- "second": None,
- "microsecond": None,
- "tz": None,
- "quarter": None,
- "day_of_week": None,
- "day_of_year": None,
- "meridiem": None,
- "timestamp": None,
- }
-
- pattern = self._FROM_FORMAT_RE.sub(
- lambda m: self._replace_tokens(m.group(0), locale), escaped_fmt
- )
-
- if not re.search("^" + pattern + "$", time):
- raise ValueError("String does not match format {}".format(fmt))
-
- re.sub(pattern, lambda m: self._get_parsed_values(m, parsed, locale, now), time)
-
- return self._check_parsed(parsed, now)
-
- def _check_parsed(
- self, parsed, now
- ): # type: (typing.Dict[str, typing.Any], pendulum.DateTime) -> typing.Dict[str, typing.Any]
- """
- Checks validity of parsed elements.
-
- :param parsed: The elements to parse.
-
- :return: The validated elements.
- """
- validated = {
- "year": parsed["year"],
- "month": parsed["month"],
- "day": parsed["day"],
- "hour": parsed["hour"],
- "minute": parsed["minute"],
- "second": parsed["second"],
- "microsecond": parsed["microsecond"],
- "tz": None,
- }
-
- # If timestamp has been specified
- # we use it and don't go any further
- if parsed["timestamp"] is not None:
- str_us = str(parsed["timestamp"])
- if "." in str_us:
- microseconds = int("{}".format(str_us.split(".")[1].ljust(6, "0")))
- else:
- microseconds = 0
-
- from pendulum.helpers import local_time
-
- time = local_time(parsed["timestamp"], 0, microseconds)
- validated["year"] = time[0]
- validated["month"] = time[1]
- validated["day"] = time[2]
- validated["hour"] = time[3]
- validated["minute"] = time[4]
- validated["second"] = time[5]
- validated["microsecond"] = time[6]
-
- return validated
-
- if parsed["quarter"] is not None:
- if validated["year"] is not None:
- dt = pendulum.datetime(validated["year"], 1, 1)
- else:
- dt = now
-
- dt = dt.start_of("year")
-
- while dt.quarter != parsed["quarter"]:
- dt = dt.add(months=3)
-
- validated["year"] = dt.year
- validated["month"] = dt.month
- validated["day"] = dt.day
-
- if validated["year"] is None:
- validated["year"] = now.year
-
- if parsed["day_of_year"] is not None:
- dt = pendulum.parse(
- "{}-{:>03d}".format(validated["year"], parsed["day_of_year"])
- )
-
- validated["month"] = dt.month
- validated["day"] = dt.day
-
- if parsed["day_of_week"] is not None:
- dt = pendulum.datetime(
- validated["year"],
- validated["month"] or now.month,
- validated["day"] or now.day,
- )
- dt = dt.start_of("week").subtract(days=1)
- dt = dt.next(parsed["day_of_week"])
- validated["year"] = dt.year
- validated["month"] = dt.month
- validated["day"] = dt.day
-
- # Meridiem
- if parsed["meridiem"] is not None:
- # If the time is greater than 13:00:00
- # This is not valid
- if validated["hour"] is None:
- raise ValueError("Invalid Date")
-
- t = (
- validated["hour"],
- validated["minute"],
- validated["second"],
- validated["microsecond"],
- )
- if t >= (13, 0, 0, 0):
- raise ValueError("Invalid date")
-
- pm = parsed["meridiem"] == "pm"
- validated["hour"] %= 12
- if pm:
- validated["hour"] += 12
-
- if validated["month"] is None:
- if parsed["year"] is not None:
- validated["month"] = parsed["month"] or 1
- else:
- validated["month"] = parsed["month"] or now.month
-
- if validated["day"] is None:
- if parsed["year"] is not None or parsed["month"] is not None:
- validated["day"] = parsed["day"] or 1
- else:
- validated["day"] = parsed["day"] or now.day
-
- for part in ["hour", "minute", "second", "microsecond"]:
- if validated[part] is None:
- validated[part] = 0
-
- validated["tz"] = parsed["tz"]
-
- return validated
-
- def _get_parsed_values(
- self, m, parsed, locale, now
- ): # type: (typing.Match[str], typing.Dict[str, typing.Any], Locale, pendulum.DateTime) -> None
- for token, index in m.re.groupindex.items():
- if token in self._LOCALIZABLE_TOKENS:
- self._get_parsed_locale_value(token, m.group(index), parsed, locale)
- else:
- self._get_parsed_value(token, m.group(index), parsed, now)
-
- def _get_parsed_value(
- self, token, value, parsed, now
- ): # type: (str, str, typing.Dict[str, typing.Any], pendulum.DateTime) -> None
- parsed_token = self._PARSE_TOKENS[token](value)
-
- if "Y" in token:
- if token == "YY":
- parsed_token = now.year // 100 * 100 + parsed_token
-
- parsed["year"] = parsed_token
- elif "Q" == token:
- parsed["quarter"] = parsed_token
- elif token in ["MM", "M"]:
- parsed["month"] = parsed_token
- elif token in ["DDDD", "DDD"]:
- parsed["day_of_year"] = parsed_token
- elif "D" in token:
- parsed["day"] = parsed_token
- elif "H" in token:
- parsed["hour"] = parsed_token
- elif token in ["hh", "h"]:
- if parsed_token > 12:
- raise ValueError("Invalid date")
-
- parsed["hour"] = parsed_token
- elif "m" in token:
- parsed["minute"] = parsed_token
- elif "s" in token:
- parsed["second"] = parsed_token
- elif "S" in token:
- parsed["microsecond"] = parsed_token
- elif token in ["d", "E"]:
- parsed["day_of_week"] = parsed_token
- elif token in ["X", "x"]:
- parsed["timestamp"] = parsed_token
- elif token in ["ZZ", "Z"]:
- negative = True if value.startswith("-") else False
- tz = value[1:]
- if ":" not in tz:
- if len(tz) == 2:
- tz = "{}00".format(tz)
-
- off_hour = tz[0:2]
- off_minute = tz[2:4]
- else:
- off_hour, off_minute = tz.split(":")
-
- offset = ((int(off_hour) * 60) + int(off_minute)) * 60
-
- if negative:
- offset = -1 * offset
-
- parsed["tz"] = pendulum.timezone(offset)
- elif token == "z":
- # Full timezone
- if value not in pendulum.timezones:
- raise ValueError("Invalid date")
-
- parsed["tz"] = pendulum.timezone(value)
-
- def _get_parsed_locale_value(
- self, token, value, parsed, locale
- ): # type: (str, str, typing.Dict[str, typing.Any], Locale) -> None
- if token == "MMMM":
- unit = "month"
- match = "months.wide"
- elif token == "MMM":
- unit = "month"
- match = "months.abbreviated"
- elif token == "Do":
- parsed["day"] = int(re.match(r"(\d+)", value).group(1))
-
- return
- elif token == "dddd":
- unit = "day_of_week"
- match = "days.wide"
- elif token == "ddd":
- unit = "day_of_week"
- match = "days.abbreviated"
- elif token == "dd":
- unit = "day_of_week"
- match = "days.short"
- elif token in ["a", "A"]:
- valid_values = [
- locale.translation("day_periods.am"),
- locale.translation("day_periods.pm"),
- ]
-
- if token == "a":
- value = value.lower()
- valid_values = list(map(lambda x: x.lower(), valid_values))
-
- if value not in valid_values:
- raise ValueError("Invalid date")
-
- parsed["meridiem"] = ["am", "pm"][valid_values.index(value)]
-
- return
- else:
- raise ValueError('Invalid token "{}"'.format(token))
-
- parsed[unit] = locale.match_translation(match, value)
- if value is None:
- raise ValueError("Invalid date")
-
- def _replace_tokens(self, token, locale): # type: (str, Locale) -> str
- if token.startswith("[") and token.endswith("]"):
- return token[1:-1]
- elif token.startswith("\\"):
- if len(token) == 2 and token[1] in {"[", "]"}:
- return ""
-
- return token
- elif token not in self._REGEX_TOKENS and token not in self._LOCALIZABLE_TOKENS:
- raise ValueError("Unsupported token: {}".format(token))
-
- if token in self._LOCALIZABLE_TOKENS:
- values = self._LOCALIZABLE_TOKENS[token]
- if callable(values):
- candidates = values(locale)
- else:
- candidates = tuple(
- locale.translation(self._LOCALIZABLE_TOKENS[token]).values()
- )
- else:
- candidates = self._REGEX_TOKENS[token]
-
- if not candidates:
- raise ValueError("Unsupported token: {}".format(token))
-
- if not isinstance(candidates, tuple):
- candidates = (candidates,)
-
- pattern = "(?P<{}>{})".format(token, "|".join([decode(p) for p in candidates]))
-
- return pattern
+from __future__ import annotations
+
+import datetime
+import re
+
+from typing import TYPE_CHECKING
+from typing import Any
+from typing import Callable
+from typing import Match
+from typing import Sequence
+from typing import cast
+
+import pendulum
+
+from pendulum.locales.locale import Locale
+
+if TYPE_CHECKING:
+ from pendulum import Timezone
+
+_MATCH_1 = r"\d"
+_MATCH_2 = r"\d\d"
+_MATCH_3 = r"\d{3}"
+_MATCH_4 = r"\d{4}"
+_MATCH_6 = r"[+-]?\d{6}"
+_MATCH_1_TO_2 = r"\d\d?"
+_MATCH_1_TO_2_LEFT_PAD = r"[0-9 ]\d?"
+_MATCH_1_TO_3 = r"\d{1,3}"
+_MATCH_1_TO_4 = r"\d{1,4}"
+_MATCH_1_TO_6 = r"[+-]?\d{1,6}"
+_MATCH_3_TO_4 = r"\d{3}\d?"
+_MATCH_5_TO_6 = r"\d{5}\d?"
+_MATCH_UNSIGNED = r"\d+"
+_MATCH_SIGNED = r"[+-]?\d+"
+_MATCH_OFFSET = r"[Zz]|[+-]\d\d:?\d\d"
+_MATCH_SHORT_OFFSET = r"[Zz]|[+-]\d\d(?::?\d\d)?"
+_MATCH_TIMESTAMP = r"[+-]?\d+(\.\d{1,6})?"
+_MATCH_WORD = (
+ "(?i)[0-9]*"
+ "['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+"
+ r"|[\u0600-\u06FF/]+(\s*?[\u0600-\u06FF]+){1,2}"
+)
+_MATCH_TIMEZONE = "[A-Za-z0-9-+]+(/[A-Za-z0-9-+_]+)?"
+
+
+class Formatter:
+ _TOKENS: str = (
+ r"\[([^\[]*)\]|\\(.)|"
+ "("
+ "Mo|MM?M?M?"
+ "|Do|DDDo|DD?D?D?|ddd?d?|do?"
+ "|E{1,4}"
+ "|w[o|w]?|W[o|W]?|Qo?"
+ "|YYYY|YY|Y"
+ "|gg(ggg?)?|GG(GGG?)?"
+ "|a|A"
+ "|hh?|HH?|kk?"
+ "|mm?|ss?|S{1,9}"
+ "|x|X"
+ "|zz?|ZZ?"
+ "|LTS|LT|LL?L?L?"
+ ")"
+ )
+
+ _FORMAT_RE: re.Pattern[str] = re.compile(_TOKENS)
+
+ _FROM_FORMAT_RE: re.Pattern[str] = re.compile(r"(?<!\\\[)" + _TOKENS + r"(?!\\\])")
+
+ _LOCALIZABLE_TOKENS: dict[str, str | Callable[[Locale], Sequence[str]] | None] = {
+ "Qo": None,
+ "MMMM": "months.wide",
+ "MMM": "months.abbreviated",
+ "Mo": None,
+ "DDDo": None,
+ "Do": lambda locale: tuple(
+ rf"\d+{o}" for o in locale.get("custom.ordinal").values()
+ ),
+ "dddd": "days.wide",
+ "ddd": "days.abbreviated",
+ "dd": "days.short",
+ "do": None,
+ "Wo": None,
+ "wo": None,
+ "A": lambda locale: (
+ locale.translation("day_periods.am"),
+ locale.translation("day_periods.pm"),
+ ),
+ "a": lambda locale: (
+ locale.translation("day_periods.am").lower(),
+ locale.translation("day_periods.pm").lower(),
+ ),
+ }
+
+ _TOKENS_RULES: dict[str, Callable[[pendulum.DateTime], str]] = {
+ # Year
+ "YYYY": lambda dt: f"{dt.year:d}",
+ "YY": lambda dt: f"{dt.year:d}"[2:],
+ "Y": lambda dt: f"{dt.year:d}",
+ # Quarter
+ "Q": lambda dt: f"{dt.quarter:d}",
+ # Month
+ "MM": lambda dt: f"{dt.month:02d}",
+ "M": lambda dt: f"{dt.month:d}",
+ # Day
+ "DD": lambda dt: f"{dt.day:02d}",
+ "D": lambda dt: f"{dt.day:d}",
+ # Day of Year
+ "DDDD": lambda dt: f"{dt.day_of_year:03d}",
+ "DDD": lambda dt: f"{dt.day_of_year:d}",
+ # Day of Week
+ "d": lambda dt: f"{dt.day_of_week:d}",
+ # Day of ISO Week
+ "E": lambda dt: f"{dt.isoweekday():d}",
+ # Hour
+ "HH": lambda dt: f"{dt.hour:02d}",
+ "H": lambda dt: f"{dt.hour:d}",
+ "hh": lambda dt: f"{dt.hour % 12 or 12:02d}",
+ "h": lambda dt: f"{dt.hour % 12 or 12:d}",
+ # Minute
+ "mm": lambda dt: f"{dt.minute:02d}",
+ "m": lambda dt: f"{dt.minute:d}",
+ # Second
+ "ss": lambda dt: f"{dt.second:02d}",
+ "s": lambda dt: f"{dt.second:d}",
+ # Fractional second
+ "S": lambda dt: f"{dt.microsecond // 100000:01d}",
+ "SS": lambda dt: f"{dt.microsecond // 10000:02d}",
+ "SSS": lambda dt: f"{dt.microsecond // 1000:03d}",
+ "SSSS": lambda dt: f"{dt.microsecond // 100:04d}",
+ "SSSSS": lambda dt: f"{dt.microsecond // 10:05d}",
+ "SSSSSS": lambda dt: f"{dt.microsecond:06d}",
+ # Timestamp
+ "X": lambda dt: f"{dt.int_timestamp:d}",
+ "x": lambda dt: f"{dt.int_timestamp * 1000 + dt.microsecond // 1000:d}",
+ # Timezone
+ "zz": lambda dt: f'{dt.tzname() if dt.tzinfo is not None else ""}',
+ "z": lambda dt: f'{dt.timezone_name or ""}',
+ }
+
+ _DATE_FORMATS = {
+ "LTS": "formats.time.full",
+ "LT": "formats.time.short",
+ "L": "formats.date.short",
+ "LL": "formats.date.long",
+ "LLL": "formats.datetime.long",
+ "LLLL": "formats.datetime.full",
+ }
+
+ _DEFAULT_DATE_FORMATS = {
+ "LTS": "h:mm:ss A",
+ "LT": "h:mm A",
+ "L": "MM/DD/YYYY",
+ "LL": "MMMM D, YYYY",
+ "LLL": "MMMM D, YYYY h:mm A",
+ "LLLL": "dddd, MMMM D, YYYY h:mm A",
+ }
+
+ _REGEX_TOKENS: dict[str, str | Sequence[str] | None] = {
+ "Y": _MATCH_SIGNED,
+ "YY": (_MATCH_1_TO_2, _MATCH_2),
+ "YYYY": (_MATCH_1_TO_4, _MATCH_4),
+ "Q": _MATCH_1,
+ "Qo": None,
+ "M": _MATCH_1_TO_2,
+ "MM": (_MATCH_1_TO_2, _MATCH_2),
+ "MMM": _MATCH_WORD,
+ "MMMM": _MATCH_WORD,
+ "D": _MATCH_1_TO_2,
+ "DD": (_MATCH_1_TO_2_LEFT_PAD, _MATCH_2),
+ "DDD": _MATCH_1_TO_3,
+ "DDDD": _MATCH_3,
+ "dddd": _MATCH_WORD,
+ "ddd": _MATCH_WORD,
+ "dd": _MATCH_WORD,
+ "d": _MATCH_1,
+ "E": _MATCH_1,
+ "Do": None,
+ "H": _MATCH_1_TO_2,
+ "HH": (_MATCH_1_TO_2, _MATCH_2),
+ "h": _MATCH_1_TO_2,
+ "hh": (_MATCH_1_TO_2, _MATCH_2),
+ "m": _MATCH_1_TO_2,
+ "mm": (_MATCH_1_TO_2, _MATCH_2),
+ "s": _MATCH_1_TO_2,
+ "ss": (_MATCH_1_TO_2, _MATCH_2),
+ "S": (_MATCH_1_TO_3, _MATCH_1),
+ "SS": (_MATCH_1_TO_3, _MATCH_2),
+ "SSS": (_MATCH_1_TO_3, _MATCH_3),
+ "SSSS": _MATCH_UNSIGNED,
+ "SSSSS": _MATCH_UNSIGNED,
+ "SSSSSS": _MATCH_UNSIGNED,
+ "x": _MATCH_SIGNED,
+ "X": _MATCH_TIMESTAMP,
+ "ZZ": _MATCH_SHORT_OFFSET,
+ "Z": _MATCH_OFFSET,
+ "z": _MATCH_TIMEZONE,
+ }
+
+ _PARSE_TOKENS: dict[str, Callable[[str], Any]] = {
+ "YYYY": lambda year: int(year),
+ "YY": lambda year: int(year),
+ "Q": lambda quarter: int(quarter),
+ "MMMM": lambda month: month,
+ "MMM": lambda month: month,
+ "MM": lambda month: int(month),
+ "M": lambda month: int(month),
+ "DDDD": lambda day: int(day),
+ "DDD": lambda day: int(day),
+ "DD": lambda day: int(day),
+ "D": lambda day: int(day),
+ "dddd": lambda weekday: weekday,
+ "ddd": lambda weekday: weekday,
+ "dd": lambda weekday: weekday,
+ "d": lambda weekday: int(weekday) % 7,
+ "E": lambda weekday: int(weekday),
+ "HH": lambda hour: int(hour),
+ "H": lambda hour: int(hour),
+ "hh": lambda hour: int(hour),
+ "h": lambda hour: int(hour),
+ "mm": lambda minute: int(minute),
+ "m": lambda minute: int(minute),
+ "ss": lambda second: int(second),
+ "s": lambda second: int(second),
+ "S": lambda us: int(us) * 100000,
+ "SS": lambda us: int(us) * 10000,
+ "SSS": lambda us: int(us) * 1000,
+ "SSSS": lambda us: int(us) * 100,
+ "SSSSS": lambda us: int(us) * 10,
+ "SSSSSS": lambda us: int(us),
+ "a": lambda meridiem: meridiem,
+ "X": lambda ts: float(ts),
+ "x": lambda ts: float(ts) / 1e3,
+ "ZZ": str,
+ "Z": str,
+ "z": str,
+ }
+
+ def format(
+ self, dt: pendulum.DateTime, fmt: str, locale: str | Locale | None = None
+ ) -> str:
+ """
+ Formats a DateTime instance with a given format and locale.
+
+ :param dt: The instance to format
+ :param fmt: The format to use
+ :param locale: The locale to use
+ """
+ loaded_locale: Locale = Locale.load(locale or pendulum.get_locale())
+
+ result = self._FORMAT_RE.sub(
+ lambda m: m.group(1)
+ if m.group(1)
+ else m.group(2)
+ if m.group(2)
+ else self._format_token(dt, m.group(3), loaded_locale),
+ fmt,
+ )
+
+ return result
+
+ def _format_token(self, dt: pendulum.DateTime, token: str, locale: Locale) -> str:
+ """
+ Formats a DateTime instance with a given token and locale.
+
+ :param dt: The instance to format
+ :param token: The token to use
+ :param locale: The locale to use
+ """
+ if token in self._DATE_FORMATS:
+ fmt = locale.get(f"custom.date_formats.{token}")
+ if fmt is None:
+ fmt = self._DEFAULT_DATE_FORMATS[token]
+
+ return self.format(dt, fmt, locale)
+
+ if token in self._LOCALIZABLE_TOKENS:
+ return self._format_localizable_token(dt, token, locale)
+
+ if token in self._TOKENS_RULES:
+ return self._TOKENS_RULES[token](dt)
+
+ # Timezone
+ if token in ["ZZ", "Z"]:
+ if dt.tzinfo is None:
+ return ""
+
+ separator = ":" if token == "Z" else ""
+ offset = dt.utcoffset() or datetime.timedelta()
+ minutes = offset.total_seconds() / 60
+
+ if minutes >= 0:
+ sign = "+"
+ else:
+ sign = "-"
+
+ hour, minute = divmod(abs(int(minutes)), 60)
+
+ return f"{sign}{hour:02d}{separator}{minute:02d}"
+
+ return token
+
+ def _format_localizable_token(
+ self, dt: pendulum.DateTime, token: str, locale: Locale
+ ) -> str:
+ """
+ Formats a DateTime instance
+ with a given localizable token and locale.
+
+ :param dt: The instance to format
+ :param token: The token to use
+ :param locale: The locale to use
+ """
+ if token == "MMM":
+ return cast(str, locale.get("translations.months.abbreviated")[dt.month])
+ elif token == "MMMM":
+ return cast(str, locale.get("translations.months.wide")[dt.month])
+ elif token == "dd":
+ return cast(str, locale.get("translations.days.short")[dt.day_of_week])
+ elif token == "ddd":
+ return cast(
+ str, locale.get("translations.days.abbreviated")[dt.day_of_week]
+ )
+ elif token == "dddd":
+ return cast(str, locale.get("translations.days.wide")[dt.day_of_week])
+ elif token == "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 cast(str, locale.get(key))
+ else:
+ return token
+
+ def parse(
+ self,
+ time: str,
+ fmt: str,
+ now: pendulum.DateTime,
+ locale: str | None = None,
+ ) -> dict[str, Any]:
+ """
+ Parses a time string matching a given format as a tuple.
+
+ :param time: The timestring
+ :param fmt: The format
+ :param now: The datetime to use as "now"
+ :param locale: The locale to use
+
+ :return: The parsed elements
+ """
+ escaped_fmt = re.escape(fmt)
+
+ tokens = self._FROM_FORMAT_RE.findall(escaped_fmt)
+ if not tokens:
+ raise ValueError("The given time string does not match the given format")
+
+ if not locale:
+ locale = pendulum.get_locale()
+
+ loaded_locale: Locale = Locale.load(locale)
+
+ parsed = {
+ "year": None,
+ "month": None,
+ "day": None,
+ "hour": None,
+ "minute": None,
+ "second": None,
+ "microsecond": None,
+ "tz": None,
+ "quarter": None,
+ "day_of_week": None,
+ "day_of_year": None,
+ "meridiem": None,
+ "timestamp": None,
+ }
+
+ pattern = self._FROM_FORMAT_RE.sub(
+ lambda m: self._replace_tokens(m.group(0), loaded_locale), escaped_fmt
+ )
+
+ if not re.search("^" + pattern + "$", time):
+ raise ValueError(f"String does not match format {fmt}")
+
+ _get_parsed_values: Callable[
+ [Match[str]], Any
+ ] = lambda m: self._get_parsed_values(m, parsed, loaded_locale, now)
+
+ re.sub(pattern, _get_parsed_values, time)
+
+ return self._check_parsed(parsed, now)
+
+ def _check_parsed(
+ self, parsed: dict[str, Any], now: pendulum.DateTime
+ ) -> dict[str, Any]:
+ """
+ Checks validity of parsed elements.
+
+ :param parsed: The elements to parse.
+
+ :return: The validated elements.
+ """
+ validated: dict[str, int | Timezone | None] = {
+ "year": parsed["year"],
+ "month": parsed["month"],
+ "day": parsed["day"],
+ "hour": parsed["hour"],
+ "minute": parsed["minute"],
+ "second": parsed["second"],
+ "microsecond": parsed["microsecond"],
+ "tz": None,
+ }
+
+ # If timestamp has been specified
+ # we use it and don't go any further
+ if parsed["timestamp"] is not None:
+ str_us = str(parsed["timestamp"])
+ if "." in str_us:
+ microseconds = int(f'{str_us.split(".")[1].ljust(6, "0")}')
+ else:
+ microseconds = 0
+
+ from pendulum.helpers import local_time
+
+ time = local_time(parsed["timestamp"], 0, microseconds)
+ validated["year"] = time[0]
+ validated["month"] = time[1]
+ validated["day"] = time[2]
+ validated["hour"] = time[3]
+ validated["minute"] = time[4]
+ validated["second"] = time[5]
+ validated["microsecond"] = time[6]
+
+ return validated
+
+ if parsed["quarter"] is not None:
+ if validated["year"] is not None:
+ dt = pendulum.datetime(validated["year"], 1, 1)
+ else:
+ dt = now
+
+ dt = dt.start_of("year")
+
+ while dt.quarter != parsed["quarter"]:
+ dt = dt.add(months=3)
+
+ validated["year"] = dt.year
+ validated["month"] = dt.month
+ validated["day"] = dt.day
+
+ if validated["year"] is None:
+ validated["year"] = now.year
+
+ if parsed["day_of_year"] is not None:
+ dt = cast(
+ pendulum.DateTime,
+ pendulum.parse(f'{validated["year"]}-{parsed["day_of_year"]:>03d}'),
+ )
+
+ validated["month"] = dt.month
+ validated["day"] = dt.day
+
+ if parsed["day_of_week"] is not None:
+ dt = pendulum.datetime(
+ cast(int, validated["year"]),
+ 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: Match[str],
+ parsed: dict[str, Any],
+ locale: Locale,
+ now: pendulum.DateTime,
+ ) -> None:
+ for token, index in m.re.groupindex.items():
+ if token in self._LOCALIZABLE_TOKENS:
+ self._get_parsed_locale_value(token, m.group(index), parsed, locale)
+ else:
+ self._get_parsed_value(token, m.group(index), parsed, now)
+
+ def _get_parsed_value(
+ self,
+ token: str,
+ value: str,
+ parsed: dict[str, Any],
+ now: pendulum.DateTime,
+ ) -> None:
+ parsed_token = self._PARSE_TOKENS[token](value)
+
+ if "Y" in token:
+ if token == "YY":
+ parsed_token = now.year // 100 * 100 + parsed_token
+
+ parsed["year"] = parsed_token
+ elif token == "Q":
+ parsed["quarter"] = parsed_token
+ elif token in ["MM", "M"]:
+ parsed["month"] = parsed_token
+ elif token in ["DDDD", "DDD"]:
+ parsed["day_of_year"] = parsed_token
+ elif "D" in token:
+ parsed["day"] = parsed_token
+ elif "H" in token:
+ parsed["hour"] = parsed_token
+ elif token in ["hh", "h"]:
+ if parsed_token > 12:
+ raise ValueError("Invalid date")
+
+ parsed["hour"] = parsed_token
+ elif "m" in token:
+ parsed["minute"] = parsed_token
+ elif "s" in token:
+ parsed["second"] = parsed_token
+ elif "S" in token:
+ parsed["microsecond"] = parsed_token
+ elif token in ["d", "E"]:
+ parsed["day_of_week"] = parsed_token
+ elif token in ["X", "x"]:
+ parsed["timestamp"] = parsed_token
+ elif token in ["ZZ", "Z"]:
+ negative = bool(value.startswith("-"))
+ tz = value[1:]
+ if ":" not in tz:
+ if len(tz) == 2:
+ tz = f"{tz}00"
+
+ off_hour = tz[0:2]
+ off_minute = tz[2:4]
+ else:
+ off_hour, off_minute = tz.split(":")
+
+ offset = ((int(off_hour) * 60) + int(off_minute)) * 60
+
+ if negative:
+ offset = -1 * offset
+
+ parsed["tz"] = pendulum.timezone(offset)
+ elif token == "z":
+ # Full timezone
+ if value not in pendulum.timezones():
+ raise ValueError("Invalid date")
+
+ parsed["tz"] = pendulum.timezone(value)
+
+ def _get_parsed_locale_value(
+ self, token: str, value: str, parsed: dict[str, Any], locale: Locale
+ ) -> None:
+ if token == "MMMM":
+ unit = "month"
+ match = "months.wide"
+ elif token == "MMM":
+ unit = "month"
+ match = "months.abbreviated"
+ elif token == "Do":
+ parsed["day"] = int(cast(Match[str], re.match(r"(\d+)", value)).group(1))
+
+ return
+ elif token == "dddd":
+ unit = "day_of_week"
+ match = "days.wide"
+ elif token == "ddd":
+ unit = "day_of_week"
+ match = "days.abbreviated"
+ elif token == "dd":
+ unit = "day_of_week"
+ match = "days.short"
+ elif token in ["a", "A"]:
+ valid_values = [
+ locale.translation("day_periods.am"),
+ locale.translation("day_periods.pm"),
+ ]
+
+ if token == "a":
+ value = value.lower()
+ valid_values = [x.lower() for x in valid_values]
+
+ if value not in valid_values:
+ raise ValueError("Invalid date")
+
+ parsed["meridiem"] = ["am", "pm"][valid_values.index(value)]
+
+ return
+ else:
+ raise ValueError(f'Invalid token "{token}"')
+
+ parsed[unit] = locale.match_translation(match, value)
+ if value is None:
+ raise ValueError("Invalid date")
+
+ def _replace_tokens(self, token: str, locale: Locale) -> str:
+ if token.startswith("[") and token.endswith("]"):
+ return token[1:-1]
+ elif token.startswith("\\"):
+ if len(token) == 2 and token[1] in {"[", "]"}:
+ return ""
+
+ return token
+ elif token not in self._REGEX_TOKENS and token not in self._LOCALIZABLE_TOKENS:
+ raise ValueError(f"Unsupported token: {token}")
+
+ if token in self._LOCALIZABLE_TOKENS:
+ values = self._LOCALIZABLE_TOKENS[token]
+ if callable(values):
+ candidates = values(locale)
+ else:
+ candidates = tuple(
+ locale.translation(
+ cast(str, self._LOCALIZABLE_TOKENS[token])
+ ).values()
+ )
+ else:
+ candidates = cast(Sequence[str], self._REGEX_TOKENS[token])
+
+ if not candidates:
+ raise ValueError(f"Unsupported token: {token}")
+
+ if not isinstance(candidates, tuple):
+ candidates = (cast(str, candidates),)
+
+ pattern = f'(?P<{token}>{"|".join(candidates)})'
+
+ return pattern
diff --git a/pendulum/helpers.py b/pendulum/helpers.py
index f149ca5..13b7f22 100644
--- a/pendulum/helpers.py
+++ b/pendulum/helpers.py
@@ -1,224 +1,223 @@
-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
+from __future__ import annotations
+
+import os
+import struct
+
+from datetime import date
+from datetime import datetime
+from datetime import timedelta
+from math import copysign
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import overload
+
+import pendulum
+
+from pendulum.constants import DAYS_PER_MONTHS
+from pendulum.formatting.difference_formatter import DifferenceFormatter
+from pendulum.locales.locale import Locale
+
+if TYPE_CHECKING:
+ # Prevent import cycles
+ from pendulum.duration import Duration
+
+with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1"
+
+_DT = TypeVar("_DT", bound=datetime)
+_D = TypeVar("_D", bound=date)
+
+try:
+ # nopycln: file # noqa: E800
+ if not with_extensions or struct.calcsize("P") == 4:
+ raise ImportError()
+
+ from pendulum._extensions._helpers import PreciseDiff
+ from pendulum._extensions._helpers import days_in_year
+ from pendulum._extensions._helpers import is_leap
+ from pendulum._extensions._helpers import is_long_year
+ from pendulum._extensions._helpers import local_time
+ from pendulum._extensions._helpers import precise_diff
+ from pendulum._extensions._helpers import timestamp
+ from pendulum._extensions._helpers import week_day
+except ImportError:
+ from pendulum._extensions.helpers import PreciseDiff # type: ignore[misc]
+ from pendulum._extensions.helpers import days_in_year
+ from pendulum._extensions.helpers import is_leap
+ from pendulum._extensions.helpers import is_long_year
+ from pendulum._extensions.helpers import local_time
+ from pendulum._extensions.helpers import precise_diff # type: ignore[misc]
+ from pendulum._extensions.helpers import timestamp
+ from pendulum._extensions.helpers import week_day
+
+difference_formatter = DifferenceFormatter()
+
+
+@overload
+def add_duration(
+ dt: datetime,
+ years: int = 0,
+ months: int = 0,
+ weeks: int = 0,
+ days: int = 0,
+ hours: int = 0,
+ minutes: int = 0,
+ seconds: float = 0,
+ microseconds: int = 0,
+) -> datetime:
+ ...
+
+
+@overload
+def add_duration(
+ dt: date,
+ years: int = 0,
+ months: int = 0,
+ weeks: int = 0,
+ days: int = 0,
+) -> date:
+ pass
+
+
+def add_duration(
+ dt: date | datetime,
+ years: int = 0,
+ months: int = 0,
+ weeks: int = 0,
+ days: int = 0,
+ hours: int = 0,
+ minutes: int = 0,
+ seconds: float = 0,
+ microseconds: int = 0,
+) -> date | datetime:
+ """
+ Adds a duration to a date/datetime instance.
+ """
+ days += weeks * 7
+
+ if (
+ isinstance(dt, date)
+ and not isinstance(dt, datetime)
+ and any([hours, minutes, seconds, microseconds])
+ ):
+ raise RuntimeError("Time elements cannot be added to a date instance.")
+
+ # Normalizing
+ if abs(microseconds) > 999999:
+ s = _sign(microseconds)
+ div, mod = divmod(microseconds * s, 1000000)
+ microseconds = mod * s
+ seconds += div * s
+
+ if abs(seconds) > 59:
+ s = _sign(seconds)
+ div, mod = divmod(seconds * s, 60) # type: ignore[assignment]
+ seconds = mod * s
+ minutes += div * s
+
+ if abs(minutes) > 59:
+ s = _sign(minutes)
+ div, mod = divmod(minutes * s, 60)
+ minutes = mod * s
+ hours += div * s
+
+ if abs(hours) > 23:
+ s = _sign(hours)
+ div, mod = divmod(hours * s, 24)
+ hours = mod * s
+ days += div * s
+
+ if abs(months) > 11:
+ s = _sign(months)
+ div, mod = divmod(months * s, 12)
+ months = mod * s
+ years += div * s
+
+ year = dt.year + years
+ month = dt.month
+
+ if months:
+ month += months
+ if month > 12:
+ year += 1
+ month -= 12
+ elif month < 1:
+ year -= 1
+ month += 12
+
+ day = min(DAYS_PER_MONTHS[int(is_leap(year))][month], dt.day)
+
+ dt = dt.replace(year=year, month=month, day=day)
+
+ return dt + timedelta(
+ days=days,
+ hours=hours,
+ minutes=minutes,
+ seconds=seconds,
+ microseconds=microseconds,
+ )
+
+
+def format_diff(
+ diff: Duration,
+ is_now: bool = True,
+ absolute: bool = False,
+ locale: str | None = None,
+) -> str:
+ if locale is None:
+ locale = get_locale()
+
+ return difference_formatter.format(diff, is_now, absolute, locale)
+
+
+def _sign(x: float) -> int:
+ return int(copysign(1, x))
+
+
+# Global helpers
+
+
+def locale(name: str) -> Locale:
+ return Locale.load(name)
+
+
+def set_locale(name: str) -> None:
+ locale(name)
+
+ pendulum._LOCALE = name
+
+
+def get_locale() -> str:
+ return pendulum._LOCALE
+
+
+def week_starts_at(wday: 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: int) -> None:
+ if wday < pendulum.SUNDAY or wday > pendulum.SATURDAY:
+ raise ValueError("Invalid week day as start of week.")
+
+ pendulum._WEEK_ENDS_AT = wday
+
+
+__all__ = [
+ "PreciseDiff",
+ "days_in_year",
+ "is_leap",
+ "is_long_year",
+ "local_time",
+ "precise_diff",
+ "timestamp",
+ "week_day",
+ "add_duration",
+ "format_diff",
+ "locale",
+ "set_locale",
+ "get_locale",
+ "week_starts_at",
+ "week_ends_at",
+]
diff --git a/pendulum/interval.py b/pendulum/interval.py
new file mode 100644
index 0000000..f20042b
--- /dev/null
+++ b/pendulum/interval.py
@@ -0,0 +1,448 @@
+from __future__ import annotations
+
+import operator
+
+from datetime import date
+from datetime import datetime
+from datetime import timedelta
+from typing import TYPE_CHECKING
+from typing import Iterator
+from typing import Union
+from typing import cast
+from typing import overload
+
+import pendulum
+
+from pendulum.constants import MONTHS_PER_YEAR
+from pendulum.duration import Duration
+from pendulum.helpers import precise_diff
+
+if TYPE_CHECKING:
+ from typing import SupportsIndex
+
+ from pendulum.helpers import PreciseDiff
+ from pendulum.locales.locale import Locale # noqa
+
+
+class Interval(Duration):
+ """
+ A period of time between two datetimes.
+ """
+
+ @overload
+ def __new__(
+ cls,
+ start: pendulum.DateTime | datetime,
+ end: pendulum.DateTime | datetime,
+ absolute: bool = False,
+ ) -> Interval:
+ ...
+
+ @overload
+ def __new__(
+ cls,
+ start: pendulum.Date | date,
+ end: pendulum.Date | date,
+ absolute: bool = False,
+ ) -> Interval:
+ ...
+
+ def __new__(
+ cls,
+ start: pendulum.DateTime | pendulum.Date | datetime | date,
+ end: pendulum.DateTime | pendulum.Date | datetime | date,
+ absolute: bool = False,
+ ) -> Interval:
+ if (
+ isinstance(start, datetime)
+ and not isinstance(end, datetime)
+ or not isinstance(start, datetime)
+ and isinstance(end, datetime)
+ ):
+ raise ValueError("Both start and end of a Period must have the same type")
+
+ if (
+ isinstance(start, datetime)
+ and isinstance(end, datetime)
+ and (
+ start.tzinfo is None
+ and end.tzinfo is not None
+ or start.tzinfo is not None
+ and end.tzinfo is None
+ )
+ ):
+ raise TypeError("can't compare offset-naive and offset-aware datetimes")
+
+ if absolute and start > end:
+ end, start = start, end
+
+ _start = start
+ _end = end
+ if isinstance(start, pendulum.DateTime):
+ _start = datetime(
+ start.year,
+ start.month,
+ start.day,
+ start.hour,
+ start.minute,
+ start.second,
+ start.microsecond,
+ tzinfo=start.tzinfo,
+ fold=start.fold,
+ )
+ elif isinstance(start, pendulum.Date):
+ _start = date(start.year, start.month, start.day)
+
+ if isinstance(end, pendulum.DateTime):
+ _end = datetime(
+ end.year,
+ end.month,
+ end.day,
+ end.hour,
+ end.minute,
+ end.second,
+ end.microsecond,
+ tzinfo=end.tzinfo,
+ fold=end.fold,
+ )
+ elif isinstance(end, pendulum.Date):
+ _end = date(end.year, end.month, end.day)
+
+ # Fixing issues with datetime.__sub__()
+ # not handling offsets if the tzinfo is the same
+ if (
+ isinstance(_start, datetime)
+ and isinstance(_end, datetime)
+ and _start.tzinfo is _end.tzinfo
+ ):
+ if _start.tzinfo is not None:
+ offset = cast(timedelta, cast(datetime, start).utcoffset())
+ _start = (_start - offset).replace(tzinfo=None)
+
+ if isinstance(end, datetime) and _end.tzinfo is not None:
+ offset = cast(timedelta, end.utcoffset())
+ _end = (_end - offset).replace(tzinfo=None)
+
+ delta: timedelta = _end - _start # type: ignore[operator]
+
+ return cast(Interval, super().__new__(cls, seconds=delta.total_seconds()))
+
+ def __init__(
+ self,
+ start: pendulum.DateTime | pendulum.Date | datetime | date,
+ end: pendulum.DateTime | pendulum.Date | datetime | date,
+ absolute: bool = False,
+ ) -> None:
+ super().__init__()
+
+ _start: pendulum.DateTime | pendulum.Date | datetime | date
+ if not isinstance(start, pendulum.Date):
+ if isinstance(start, datetime):
+ start = pendulum.instance(start)
+ else:
+ start = pendulum.date(start.year, start.month, start.day)
+
+ _start = start
+ else:
+ if isinstance(start, pendulum.DateTime):
+ _start = datetime(
+ start.year,
+ start.month,
+ start.day,
+ start.hour,
+ start.minute,
+ start.second,
+ start.microsecond,
+ tzinfo=start.tzinfo,
+ )
+ else:
+ _start = date(start.year, start.month, start.day)
+
+ _end: pendulum.DateTime | pendulum.Date | datetime | date
+ if not isinstance(end, pendulum.Date):
+ if isinstance(end, datetime):
+ end = pendulum.instance(end)
+ else:
+ end = pendulum.date(end.year, end.month, end.day)
+
+ _end = end
+ else:
+ if isinstance(end, pendulum.DateTime):
+ _end = datetime(
+ end.year,
+ end.month,
+ end.day,
+ end.hour,
+ end.minute,
+ end.second,
+ end.microsecond,
+ tzinfo=end.tzinfo,
+ )
+ else:
+ _end = date(end.year, end.month, end.day)
+
+ self._invert = False
+ if start > end:
+ self._invert = True
+
+ if absolute:
+ end, start = start, end
+ _end, _start = _start, _end
+
+ self._absolute = absolute
+ self._start: pendulum.DateTime | pendulum.Date = start
+ self._end: pendulum.DateTime | pendulum.Date = end
+ self._delta: PreciseDiff = precise_diff(_start, _end)
+
+ @property
+ def years(self) -> int:
+ return self._delta.years
+
+ @property
+ def months(self) -> int:
+ return self._delta.months
+
+ @property
+ def weeks(self) -> int:
+ return abs(self._delta.days) // 7 * self._sign(self._delta.days)
+
+ @property
+ def days(self) -> int:
+ return self._days
+
+ @property
+ def remaining_days(self) -> int:
+ return abs(self._delta.days) % 7 * self._sign(self._days)
+
+ @property
+ def hours(self) -> int:
+ return self._delta.hours
+
+ @property
+ def minutes(self) -> int:
+ return self._delta.minutes
+
+ @property
+ def start(self) -> pendulum.DateTime | pendulum.Date | datetime | date:
+ return self._start
+
+ @property
+ def end(self) -> pendulum.DateTime | pendulum.Date | datetime | date:
+ return self._end
+
+ def in_years(self) -> int:
+ """
+ Gives the duration of the Period in full years.
+ """
+ return self.years
+
+ def in_months(self) -> int:
+ """
+ Gives the duration of the Period in full months.
+ """
+ return self.years * MONTHS_PER_YEAR + self.months
+
+ def in_weeks(self) -> int:
+ days = self.in_days()
+ sign = 1
+
+ if days < 0:
+ sign = -1
+
+ return sign * (abs(days) // 7)
+
+ def in_days(self) -> int:
+ return self._delta.total_days
+
+ def in_words(self, locale: str | None = None, separator: str = " ") -> str:
+ """
+ Get the current interval in words in the current locale.
+
+ Ex: 6 jours 23 heures 58 minutes
+
+ :param locale: The locale to use. Defaults to current locale.
+ :param separator: The separator to use between each unit
+ """
+ from pendulum.locales.locale import Locale # noqa
+
+ periods = [
+ ("year", self.years),
+ ("month", self.months),
+ ("week", self.weeks),
+ ("day", self.remaining_days),
+ ("hour", self.hours),
+ ("minute", self.minutes),
+ ("second", self.remaining_seconds),
+ ]
+ loaded_locale: Locale = Locale.load(locale or pendulum.get_locale())
+ parts = []
+ for period in periods:
+ unit, period_count = period
+ if abs(period_count) > 0:
+ translation = loaded_locale.translation(
+ f"units.{unit}.{loaded_locale.plural(abs(period_count))}"
+ )
+ parts.append(translation.format(period_count))
+
+ if not parts:
+ count: str | int = 0
+ if abs(self.microseconds) > 0:
+ unit = f"units.second.{loaded_locale.plural(1)}"
+ count = f"{abs(self.microseconds) / 1e6:.2f}"
+ else:
+ unit = f"units.microsecond.{loaded_locale.plural(0)}"
+
+ translation = loaded_locale.translation(unit)
+ parts.append(translation.format(count))
+
+ return separator.join(parts)
+
+ def range(
+ self, unit: str, amount: int = 1
+ ) -> Iterator[pendulum.DateTime | pendulum.Date]:
+ method = "add"
+ op = operator.le
+ if not self._absolute and self.invert:
+ method = "subtract"
+ op = operator.ge
+
+ start, end = self.start, self.end
+
+ i = amount
+ while op(start, end):
+ yield cast(Union[pendulum.DateTime, pendulum.Date], start)
+
+ start = getattr(self.start, method)(**{unit: i})
+
+ i += amount
+
+ def as_interval(self) -> Duration:
+ """
+ Return the Period as a Duration.
+ """
+ return Duration(seconds=self.total_seconds())
+
+ def __iter__(self) -> Iterator[pendulum.DateTime | pendulum.Date]:
+ return self.range("days")
+
+ def __contains__(
+ self, item: datetime | date | pendulum.DateTime | pendulum.Date
+ ) -> bool:
+ return self.start <= item <= self.end
+
+ def __add__(self, other: timedelta) -> Duration:
+ return self.as_interval().__add__(other)
+
+ __radd__ = __add__
+
+ def __sub__(self, other: timedelta) -> Duration:
+ return self.as_interval().__sub__(other)
+
+ def __neg__(self) -> Interval:
+ return self.__class__(self.end, self.start, self._absolute)
+
+ def __mul__(self, other: int | float) -> Duration:
+ return self.as_interval().__mul__(other)
+
+ __rmul__ = __mul__
+
+ @overload
+ def __floordiv__(self, other: timedelta) -> int:
+ ...
+
+ @overload
+ def __floordiv__(self, other: int) -> Duration:
+ ...
+
+ def __floordiv__(self, other: int | timedelta) -> int | Duration:
+ return self.as_interval().__floordiv__(other)
+
+ __div__ = __floordiv__ # type: ignore[assignment]
+
+ @overload
+ def __truediv__(self, other: timedelta) -> float:
+ ...
+
+ @overload
+ def __truediv__(self, other: float) -> Duration:
+ ...
+
+ def __truediv__(self, other: float | timedelta) -> Duration | float:
+ return self.as_interval().__truediv__(other)
+
+ def __mod__(self, other: timedelta) -> Duration:
+ return self.as_interval().__mod__(other)
+
+ def __divmod__(self, other: timedelta) -> tuple[int, Duration]:
+ return self.as_interval().__divmod__(other)
+
+ def __abs__(self) -> Interval:
+ return self.__class__(self.start, self.end, absolute=True)
+
+ def __repr__(self) -> str:
+ return f"<Period [{self._start} -> {self._end}]>"
+
+ def __str__(self) -> str:
+ return self.__repr__()
+
+ def _cmp(self, other: timedelta) -> int:
+ # Only needed for PyPy
+ assert isinstance(other, timedelta)
+
+ if isinstance(other, Interval):
+ other = other.as_timedelta()
+
+ td = self.as_timedelta()
+
+ return 0 if td == other else 1 if td > other else -1
+
+ def _getstate(
+ self, protocol: SupportsIndex = 3
+ ) -> tuple[
+ pendulum.DateTime | pendulum.Date | datetime | date,
+ pendulum.DateTime | pendulum.Date | datetime | date,
+ bool,
+ ]:
+ start, end = self.start, self.end
+
+ if self._invert and self._absolute:
+ end, start = start, end
+
+ return start, end, self._absolute
+
+ def __reduce__(
+ self,
+ ) -> tuple[
+ type[Interval],
+ tuple[
+ pendulum.DateTime | pendulum.Date | datetime | date,
+ pendulum.DateTime | pendulum.Date | datetime | date,
+ bool,
+ ],
+ ]:
+ return self.__reduce_ex__(2)
+
+ def __reduce_ex__(
+ self, protocol: SupportsIndex
+ ) -> tuple[
+ type[Interval],
+ tuple[
+ pendulum.DateTime | pendulum.Date | datetime | date,
+ pendulum.DateTime | pendulum.Date | datetime | date,
+ bool,
+ ],
+ ]:
+ return self.__class__, self._getstate(protocol)
+
+ def __hash__(self) -> int:
+ return hash((self.start, self.end, self._absolute))
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, Interval):
+ return (self.start, self.end, self._absolute) == (
+ other.start,
+ other.end,
+ other._absolute,
+ )
+ else:
+ return self.as_interval() == other
diff --git a/pendulum/locales/cs/__init__.py b/pendulum/locales/cs/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/cs/__init__.py
diff --git a/pendulum/locales/cs/custom.py b/pendulum/locales/cs/custom.py
new file mode 100644
index 0000000..5f66b69
--- /dev/null
+++ b/pendulum/locales/cs/custom.py
@@ -0,0 +1,23 @@
+"""
+cs custom locale file.
+"""
+
+translations = {
+ "units": {"few_second": "pár vteřin"},
+ # Relative time
+ "ago": "{} zpět",
+ "from_now": "za {}",
+ "after": "{0} po",
+ "before": "{0} zpět",
+ # Ordinals
+ "ordinal": {"one": ".", "two": ".", "few": ".", "other": "."},
+ # Date formats
+ "date_formats": {
+ "LTS": "h:mm:ss",
+ "LT": "h:mm",
+ "L": "DD. M. YYYY",
+ "LL": "D. MMMM, YYYY",
+ "LLL": "D. MMMM, YYYY h:mm",
+ "LLLL": "dddd, D. MMMM, YYYY h:mm",
+ },
+}
diff --git a/pendulum/locales/cs/locale.py b/pendulum/locales/cs/locale.py
new file mode 100644
index 0000000..2c51c78
--- /dev/null
+++ b/pendulum/locales/cs/locale.py
@@ -0,0 +1,266 @@
+from .custom import translations as custom_translations
+
+
+"""
+cs locale file.
+
+It has been generated automatically and must not be modified directly.
+"""
+
+
+locale = {
+ "plural": lambda n: "few"
+ if ((n == n and (n >= 2 and n <= 4)) and (0 == 0 and (0 == 0)))
+ else "many"
+ if (not (0 == 0 and (0 == 0)))
+ else "one"
+ if ((n == n and (n == 1)) and (0 == 0 and (0 == 0)))
+ else "other",
+ "ordinal": lambda n: "other",
+ "translations": {
+ "days": {
+ "abbreviated": {
+ 0: "ne",
+ 1: "po",
+ 2: "út",
+ 3: "st",
+ 4: "čt",
+ 5: "pá",
+ 6: "so",
+ },
+ "narrow": {
+ 0: "N",
+ 1: "P",
+ 2: "Ú",
+ 3: "S",
+ 4: "Č",
+ 5: "P",
+ 6: "S",
+ },
+ "short": {
+ 0: "ne",
+ 1: "po",
+ 2: "út",
+ 3: "st",
+ 4: "čt",
+ 5: "pá",
+ 6: "so",
+ },
+ "wide": {
+ 0: "neděle",
+ 1: "pondělí",
+ 2: "úterý",
+ 3: "středa",
+ 4: "čtvrtek",
+ 5: "pátek",
+ 6: "sobota",
+ },
+ },
+ "months": {
+ "abbreviated": {
+ 1: "led",
+ 2: "úno",
+ 3: "bře",
+ 4: "dub",
+ 5: "kvě",
+ 6: "čvn",
+ 7: "čvc",
+ 8: "srp",
+ 9: "zář",
+ 10: "říj",
+ 11: "lis",
+ 12: "pro",
+ },
+ "narrow": {
+ 1: "1",
+ 2: "2",
+ 3: "3",
+ 4: "4",
+ 5: "5",
+ 6: "6",
+ 7: "7",
+ 8: "8",
+ 9: "9",
+ 10: "10",
+ 11: "11",
+ 12: "12",
+ },
+ "wide": {
+ 1: "ledna",
+ 2: "února",
+ 3: "března",
+ 4: "dubna",
+ 5: "května",
+ 6: "června",
+ 7: "července",
+ 8: "srpna",
+ 9: "září",
+ 10: "října",
+ 11: "listopadu",
+ 12: "prosince",
+ },
+ },
+ "units": {
+ "year": {
+ "one": "{0} rok",
+ "few": "{0} roky",
+ "many": "{0} roku",
+ "other": "{0} let",
+ },
+ "month": {
+ "one": "{0} měsíc",
+ "few": "{0} měsíce",
+ "many": "{0} měsíce",
+ "other": "{0} měsíců",
+ },
+ "week": {
+ "one": "{0} týden",
+ "few": "{0} týdny",
+ "many": "{0} týdne",
+ "other": "{0} týdnů",
+ },
+ "day": {
+ "one": "{0} den",
+ "few": "{0} dny",
+ "many": "{0} dne",
+ "other": "{0} dní",
+ },
+ "hour": {
+ "one": "{0} hodina",
+ "few": "{0} hodiny",
+ "many": "{0} hodiny",
+ "other": "{0} hodin",
+ },
+ "minute": {
+ "one": "{0} minuta",
+ "few": "{0} minuty",
+ "many": "{0} minuty",
+ "other": "{0} minut",
+ },
+ "second": {
+ "one": "{0} sekunda",
+ "few": "{0} sekundy",
+ "many": "{0} sekundy",
+ "other": "{0} sekund",
+ },
+ "microsecond": {
+ "one": "{0} mikrosekunda",
+ "few": "{0} mikrosekundy",
+ "many": "{0} mikrosekundy",
+ "other": "{0} mikrosekund",
+ },
+ },
+ "relative": {
+ "year": {
+ "future": {
+ "other": "za {0} let",
+ "one": "za {0} rok",
+ "few": "za {0} roky",
+ "many": "za {0} roku",
+ },
+ "past": {
+ "other": "před {0} lety",
+ "one": "před {0} rokem",
+ "few": "před {0} lety",
+ "many": "před {0} roku",
+ },
+ },
+ "month": {
+ "future": {
+ "other": "za {0} měsíců",
+ "one": "za {0} měsíc",
+ "few": "za {0} měsíce",
+ "many": "za {0} měsíce",
+ },
+ "past": {
+ "other": "před {0} měsíci",
+ "one": "před {0} měsícem",
+ "few": "před {0} měsíci",
+ "many": "před {0} měsíce",
+ },
+ },
+ "week": {
+ "future": {
+ "other": "za {0} týdnů",
+ "one": "za {0} týden",
+ "few": "za {0} týdny",
+ "many": "za {0} týdne",
+ },
+ "past": {
+ "other": "před {0} týdny",
+ "one": "před {0} týdnem",
+ "few": "před {0} týdny",
+ "many": "před {0} týdne",
+ },
+ },
+ "day": {
+ "future": {
+ "other": "za {0} dní",
+ "one": "za {0} den",
+ "few": "za {0} dny",
+ "many": "za {0} dne",
+ },
+ "past": {
+ "other": "před {0} dny",
+ "one": "před {0} dnem",
+ "few": "před {0} dny",
+ "many": "před {0} dne",
+ },
+ },
+ "hour": {
+ "future": {
+ "other": "za {0} hodin",
+ "one": "za {0} hodinu",
+ "few": "za {0} hodiny",
+ "many": "za {0} hodiny",
+ },
+ "past": {
+ "other": "před {0} hodinami",
+ "one": "před {0} hodinou",
+ "few": "před {0} hodinami",
+ "many": "před {0} hodiny",
+ },
+ },
+ "minute": {
+ "future": {
+ "other": "za {0} minut",
+ "one": "za {0} minutu",
+ "few": "za {0} minuty",
+ "many": "za {0} minuty",
+ },
+ "past": {
+ "other": "před {0} minutami",
+ "one": "před {0} minutou",
+ "few": "před {0} minutami",
+ "many": "před {0} minuty",
+ },
+ },
+ "second": {
+ "future": {
+ "other": "za {0} sekund",
+ "one": "za {0} sekundu",
+ "few": "za {0} sekundy",
+ "many": "za {0} sekundy",
+ },
+ "past": {
+ "other": "před {0} sekundami",
+ "one": "před {0} sekundou",
+ "few": "před {0} sekundami",
+ "many": "před {0} sekundy",
+ },
+ },
+ },
+ "day_periods": {
+ "midnight": "půlnoc",
+ "am": "dop.",
+ "noon": "poledne",
+ "pm": "odp.",
+ "morning1": "ráno",
+ "morning2": "dopoledne",
+ "afternoon1": "odpoledne",
+ "evening1": "večer",
+ "night1": "v noci",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/da/custom.py b/pendulum/locales/da/custom.py
index 258e47b..c62ab83 100644
--- a/pendulum/locales/da/custom.py
+++ b/pendulum/locales/da/custom.py
@@ -1,22 +1,18 @@
-# -*- 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",
- },
-}
+"""
+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
index b829e34..936af3a 100644
--- a/pendulum/locales/da/locale.py
+++ b/pendulum/locales/da/locale.py
@@ -1,150 +1,147 @@
-# -*- 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,
-}
+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/custom.py b/pendulum/locales/de/custom.py
index 3024f0b..a19a8e1 100644
--- a/pendulum/locales/de/custom.py
+++ b/pendulum/locales/de/custom.py
@@ -1,40 +1,36 @@
-# -*- 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",
- },
-}
+"""
+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
index b180fc5..94d2ff1 100644
--- a/pendulum/locales/de/locale.py
+++ b/pendulum/locales/de/locale.py
@@ -1,147 +1,144 @@
-# -*- 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,
-}
+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/custom.py b/pendulum/locales/en/custom.py
index de224e0..a403ad8 100644
--- a/pendulum/locales/en/custom.py
+++ b/pendulum/locales/en/custom.py
@@ -1,27 +1,23 @@
-# -*- 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",
- },
-}
+"""
+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
index acee4d2..00eafc2 100644
--- a/pendulum/locales/en/locale.py
+++ b/pendulum/locales/en/locale.py
@@ -1,153 +1,150 @@
-# -*- 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,
-}
+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/custom.py b/pendulum/locales/es/custom.py
index 5862f7e..4b7e2b5 100644
--- a/pendulum/locales/es/custom.py
+++ b/pendulum/locales/es/custom.py
@@ -1,27 +1,23 @@
-# -*- 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",
- },
-}
+"""
+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
index f385e4c..edba4d3 100644
--- a/pendulum/locales/es/locale.py
+++ b/pendulum/locales/es/locale.py
@@ -1,144 +1,141 @@
-# -*- 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,
-}
+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/custom.py b/pendulum/locales/fa/custom.py
index fa5a7c1..082bfad 100644
--- a/pendulum/locales/fa/custom.py
+++ b/pendulum/locales/fa/custom.py
@@ -1,22 +1,18 @@
-# -*- 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",
- },
-}
+"""
+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
index f18b0f6..32f8e5f 100644
--- a/pendulum/locales/fa/locale.py
+++ b/pendulum/locales/fa/locale.py
@@ -1,138 +1,135 @@
-# -*- 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,
-}
+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/custom.py b/pendulum/locales/fo/custom.py
index 946ab19..456dd59 100644
--- a/pendulum/locales/fo/custom.py
+++ b/pendulum/locales/fo/custom.py
@@ -1,24 +1,20 @@
-# -*- 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",
- },
-}
+"""
+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
index 345f524..10319ea 100644
--- a/pendulum/locales/fo/locale.py
+++ b/pendulum/locales/fo/locale.py
@@ -1,135 +1,132 @@
-# -*- 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,
-}
+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
index 4c48b5a..e69de29 100644
--- a/pendulum/locales/fr/__init__.py
+++ b/pendulum/locales/fr/__init__.py
@@ -1 +0,0 @@
-# -*- coding: utf-8 -*-
diff --git a/pendulum/locales/fr/custom.py b/pendulum/locales/fr/custom.py
index 0edddbf..134f297 100644
--- a/pendulum/locales/fr/custom.py
+++ b/pendulum/locales/fr/custom.py
@@ -1,27 +1,23 @@
-# -*- 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",
- },
-}
+"""
+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
index 137c012..8855d53 100644
--- a/pendulum/locales/fr/locale.py
+++ b/pendulum/locales/fr/locale.py
@@ -1,136 +1,133 @@
-# -*- 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,
-}
+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/he/__init__.py b/pendulum/locales/he/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/he/__init__.py
diff --git a/pendulum/locales/he/custom.py b/pendulum/locales/he/custom.py
new file mode 100644
index 0000000..51f8476
--- /dev/null
+++ b/pendulum/locales/he/custom.py
@@ -0,0 +1,23 @@
+"""
+he custom locale file.
+"""
+
+translations = {
+ "units": {"few_second": "כמה שניות"},
+ # Relative time
+ "ago": "לפני {0}",
+ "from_now": "תוך {0}",
+ "after": "בעוד {0}",
+ "before": "{0} קודם",
+ # Ordinals
+ "ordinal": {"other": "º"},
+ # Date formats
+ "date_formats": {
+ "LTS": "H:mm:ss",
+ "LT": "H:mm",
+ "LLLL": "dddd, D [ב] MMMM [ב] YYYY H:mm",
+ "LLL": "D [ב] MMMM [ב] YYYY H:mm",
+ "LL": "D [ב] MMMM [ב] YYYY",
+ "L": "DD/MM/YYYY",
+ },
+}
diff --git a/pendulum/locales/he/locale.py b/pendulum/locales/he/locale.py
new file mode 100644
index 0000000..457c101
--- /dev/null
+++ b/pendulum/locales/he/locale.py
@@ -0,0 +1,269 @@
+from .custom import translations as custom_translations
+
+
+"""
+he locale file.
+
+It has been generated automatically and must not be modified directly.
+"""
+
+
+locale = {
+ "plural": lambda n: "many"
+ if (
+ ((0 == 0 and (0 == 0)) and (not (n == n and (n >= 0 and n <= 10))))
+ and ((n % 10) == (n % 10) and ((n % 10) == 0))
+ )
+ else "one"
+ if ((n == n and (n == 1)) and (0 == 0 and (0 == 0)))
+ else "two"
+ if ((n == n and (n == 2)) and (0 == 0 and (0 == 0)))
+ else "other",
+ "ordinal": lambda n: "other",
+ "translations": {
+ "days": {
+ "abbreviated": {
+ 0: "יום א׳",
+ 1: "יום ב׳",
+ 2: "יום ג׳",
+ 3: "יום ד׳",
+ 4: "יום ה׳",
+ 5: "יום ו׳",
+ 6: "שבת",
+ },
+ "narrow": {
+ 0: "א׳",
+ 1: "ב׳",
+ 2: "ג׳",
+ 3: "ד׳",
+ 4: "ה׳",
+ 5: "ו׳",
+ 6: "ש׳",
+ },
+ "short": {
+ 0: "א׳",
+ 1: "ב׳",
+ 2: "ג׳",
+ 3: "ד׳",
+ 4: "ה׳",
+ 5: "ו׳",
+ 6: "ש׳",
+ },
+ "wide": {
+ 0: "יום ראשון",
+ 1: "יום שני",
+ 2: "יום שלישי",
+ 3: "יום רביעי",
+ 4: "יום חמישי",
+ 5: "יום שישי",
+ 6: "יום שבת",
+ },
+ },
+ "months": {
+ "abbreviated": {
+ 1: "ינו׳",
+ 2: "פבר׳",
+ 3: "מרץ",
+ 4: "אפר׳",
+ 5: "מאי",
+ 6: "יוני",
+ 7: "יולי",
+ 8: "אוג׳",
+ 9: "ספט׳",
+ 10: "אוק׳",
+ 11: "נוב׳",
+ 12: "דצמ׳",
+ },
+ "narrow": {
+ 1: "1",
+ 2: "2",
+ 3: "3",
+ 4: "4",
+ 5: "5",
+ 6: "6",
+ 7: "7",
+ 8: "8",
+ 9: "9",
+ 10: "10",
+ 11: "11",
+ 12: "12",
+ },
+ "wide": {
+ 1: "ינואר",
+ 2: "פברואר",
+ 3: "מרץ",
+ 4: "אפריל",
+ 5: "מאי",
+ 6: "יוני",
+ 7: "יולי",
+ 8: "אוגוסט",
+ 9: "ספטמבר",
+ 10: "אוקטובר",
+ 11: "נובמבר",
+ 12: "דצמבר",
+ },
+ },
+ "units": {
+ "year": {
+ "one": "שנה",
+ "two": "שנתיים",
+ "many": "{0} שנים",
+ "other": "{0} שנים",
+ },
+ "month": {
+ "one": "חודש",
+ "two": "חודשיים",
+ "many": "{0} חודשים",
+ "other": "{0} חודשים",
+ },
+ "week": {
+ "one": "שבוע",
+ "two": "שבועיים",
+ "many": "{0} שבועות",
+ "other": "{0} שבועות",
+ },
+ "day": {
+ "one": "יום {0}",
+ "two": "יומיים",
+ "many": "{0} יום",
+ "other": "{0} ימים",
+ },
+ "hour": {
+ "one": "שעה",
+ "two": "שעתיים",
+ "many": "{0} שעות",
+ "other": "{0} שעות",
+ },
+ "minute": {
+ "one": "דקה",
+ "two": "שתי דקות",
+ "many": "{0} דקות",
+ "other": "{0} דקות",
+ },
+ "second": {
+ "one": "שניה",
+ "two": "שתי שניות",
+ "many": "\u200f{0} שניות",
+ "other": "{0} שניות",
+ },
+ "microsecond": {
+ "one": "{0} מיליונית שנייה",
+ "two": "{0} מיליוניות שנייה",
+ "many": "{0} מיליוניות שנייה",
+ "other": "{0} מיליוניות שנייה",
+ },
+ },
+ "relative": {
+ "year": {
+ "future": {
+ "other": "בעוד {0} שנים",
+ "one": "בעוד שנה",
+ "two": "בעוד שנתיים",
+ "many": "בעוד {0} שנה",
+ },
+ "past": {
+ "other": "לפני {0} שנים",
+ "one": "לפני שנה",
+ "two": "לפני שנתיים",
+ "many": "לפני {0} שנה",
+ },
+ },
+ "month": {
+ "future": {
+ "other": "בעוד {0} חודשים",
+ "one": "בעוד חודש",
+ "two": "בעוד חודשיים",
+ "many": "בעוד {0} חודשים",
+ },
+ "past": {
+ "other": "לפני {0} חודשים",
+ "one": "לפני חודש",
+ "two": "לפני חודשיים",
+ "many": "לפני {0} חודשים",
+ },
+ },
+ "week": {
+ "future": {
+ "other": "בעוד {0} שבועות",
+ "one": "בעוד שבוע",
+ "two": "בעוד שבועיים",
+ "many": "בעוד {0} שבועות",
+ },
+ "past": {
+ "other": "לפני {0} שבועות",
+ "one": "לפני שבוע",
+ "two": "לפני שבועיים",
+ "many": "לפני {0} שבועות",
+ },
+ },
+ "day": {
+ "future": {
+ "other": "בעוד {0} ימים",
+ "one": "בעוד יום {0}",
+ "two": "בעוד יומיים",
+ "many": "בעוד {0} ימים",
+ },
+ "past": {
+ "other": "לפני {0} ימים",
+ "one": "לפני יום {0}",
+ "two": "לפני יומיים",
+ "many": "לפני {0} ימים",
+ },
+ },
+ "hour": {
+ "future": {
+ "other": "בעוד {0} שעות",
+ "one": "בעוד שעה",
+ "two": "בעוד שעתיים",
+ "many": "בעוד {0} שעות",
+ },
+ "past": {
+ "other": "לפני {0} שעות",
+ "one": "לפני שעה",
+ "two": "לפני שעתיים",
+ "many": "לפני {0} שעות",
+ },
+ },
+ "minute": {
+ "future": {
+ "other": "בעוד {0} דקות",
+ "one": "בעוד דקה",
+ "two": "בעוד שתי דקות",
+ "many": "בעוד {0} דקות",
+ },
+ "past": {
+ "other": "לפני {0} דקות",
+ "one": "לפני דקה",
+ "two": "לפני שתי דקות",
+ "many": "לפני {0} דקות",
+ },
+ },
+ "second": {
+ "future": {
+ "other": "בעוד {0} שניות",
+ "one": "בעוד שנייה",
+ "two": "בעוד שתי שניות",
+ "many": "בעוד {0} שניות",
+ },
+ "past": {
+ "other": "לפני {0} שניות",
+ "one": "לפני שנייה",
+ "two": "לפני שתי שניות",
+ "many": "לפני {0} שניות",
+ },
+ },
+ },
+ "day_periods": {
+ "midnight": "חצות",
+ "am": "לפנה״צ",
+ "pm": "אחה״צ",
+ "morning1": "בבוקר",
+ "afternoon1": "בצהריים",
+ "afternoon2": "אחר הצהריים",
+ "evening1": "בערב",
+ "night1": "בלילה",
+ "night2": "לפנות בוקר",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/id/custom.py b/pendulum/locales/id/custom.py
index 2202481..3ba2035 100644
--- a/pendulum/locales/id/custom.py
+++ b/pendulum/locales/id/custom.py
@@ -1,23 +1,19 @@
-# -*- 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",
- },
-}
+"""
+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
index 5a3485e..bc994ce 100644
--- a/pendulum/locales/id/locale.py
+++ b/pendulum/locales/id/locale.py
@@ -1,144 +1,141 @@
-# -*- 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,
-}
+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/custom.py b/pendulum/locales/it/custom.py
index 744f55c..e5cf1cc 100644
--- a/pendulum/locales/it/custom.py
+++ b/pendulum/locales/it/custom.py
@@ -1,27 +1,24 @@
-# -*- 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",
- },
-}
+"""
+it custom locale file.
+"""
+
+
+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
index 4abf717..bb3fdcf 100644
--- a/pendulum/locales/it/locale.py
+++ b/pendulum/locales/it/locale.py
@@ -1,148 +1,145 @@
-# -*- 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,
-}
+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/ja/__init__.py b/pendulum/locales/ja/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/ja/__init__.py
diff --git a/pendulum/locales/ja/custom.py b/pendulum/locales/ja/custom.py
new file mode 100644
index 0000000..c076250
--- /dev/null
+++ b/pendulum/locales/ja/custom.py
@@ -0,0 +1,21 @@
+"""
+ja custom locale file.
+"""
+
+translations = {
+ "units": {"few_second": "数秒"},
+ # Relative time
+ "ago": "{} 前に",
+ "from_now": "今から {}",
+ "after": "{0} 後",
+ "before": "{0} 前",
+ # Date formats
+ "date_formats": {
+ "LTS": "h:mm:ss A",
+ "LT": "h:mm A",
+ "L": "MM/DD/YYYY",
+ "LL": "MMMM D, YYYY",
+ "LLL": "MMMM D, YYYY h:mm A",
+ "LLLL": "dddd, MMMM D, YYYY h:mm A",
+ },
+}
diff --git a/pendulum/locales/ja/locale.py b/pendulum/locales/ja/locale.py
new file mode 100644
index 0000000..574d2ec
--- /dev/null
+++ b/pendulum/locales/ja/locale.py
@@ -0,0 +1,194 @@
+from .custom import translations as custom_translations
+
+
+"""
+ja locale file.
+
+It has been generated automatically and must not be modified directly.
+"""
+
+
+locale = {
+ "plural": lambda n: "other",
+ "ordinal": lambda n: "other",
+ "translations": {
+ "days": {
+ "abbreviated": {
+ 0: "日",
+ 1: "月",
+ 2: "火",
+ 3: "水",
+ 4: "木",
+ 5: "金",
+ 6: "土",
+ },
+ "narrow": {
+ 0: "日",
+ 1: "月",
+ 2: "火",
+ 3: "水",
+ 4: "木",
+ 5: "金",
+ 6: "土",
+ },
+ "short": {
+ 0: "日",
+ 1: "月",
+ 2: "火",
+ 3: "水",
+ 4: "木",
+ 5: "金",
+ 6: "土",
+ },
+ "wide": {
+ 0: "日曜日",
+ 1: "月曜日",
+ 2: "火曜日",
+ 3: "水曜日",
+ 4: "木曜日",
+ 5: "金曜日",
+ 6: "土曜日",
+ },
+ },
+ "months": {
+ "abbreviated": {
+ 1: "1月",
+ 2: "2月",
+ 3: "3月",
+ 4: "4月",
+ 5: "5月",
+ 6: "6月",
+ 7: "7月",
+ 8: "8月",
+ 9: "9月",
+ 10: "10月",
+ 11: "11月",
+ 12: "12月",
+ },
+ "narrow": {
+ 1: "1",
+ 2: "2",
+ 3: "3",
+ 4: "4",
+ 5: "5",
+ 6: "6",
+ 7: "7",
+ 8: "8",
+ 9: "9",
+ 10: "10",
+ 11: "11",
+ 12: "12",
+ },
+ "wide": {
+ 1: "1月",
+ 2: "2月",
+ 3: "3月",
+ 4: "4月",
+ 5: "5月",
+ 6: "6月",
+ 7: "7月",
+ 8: "8月",
+ 9: "9月",
+ 10: "10月",
+ 11: "11月",
+ 12: "12月",
+ },
+ },
+ "units": {
+ "year": {
+ "other": "{0} 年",
+ },
+ "month": {
+ "other": "{0} か月",
+ },
+ "week": {
+ "other": "{0} 週間",
+ },
+ "day": {
+ "other": "{0} 日",
+ },
+ "hour": {
+ "other": "{0} 時間",
+ },
+ "minute": {
+ "other": "{0} 分",
+ },
+ "second": {
+ "other": "{0} 秒",
+ },
+ "microsecond": {
+ "other": "{0} マイクロ秒",
+ },
+ },
+ "relative": {
+ "year": {
+ "future": {
+ "other": "{0} 年後",
+ },
+ "past": {
+ "other": "{0} 年前",
+ },
+ },
+ "month": {
+ "future": {
+ "other": "{0} か月後",
+ },
+ "past": {
+ "other": "{0} か月前",
+ },
+ },
+ "week": {
+ "future": {
+ "other": "{0} 週間後",
+ },
+ "past": {
+ "other": "{0} 週間前",
+ },
+ },
+ "day": {
+ "future": {
+ "other": "{0} 日後",
+ },
+ "past": {
+ "other": "{0} 日前",
+ },
+ },
+ "hour": {
+ "future": {
+ "other": "{0} 時間後",
+ },
+ "past": {
+ "other": "{0} 時間前",
+ },
+ },
+ "minute": {
+ "future": {
+ "other": "{0} 分後",
+ },
+ "past": {
+ "other": "{0} 分前",
+ },
+ },
+ "second": {
+ "future": {
+ "other": "{0} 秒後",
+ },
+ "past": {
+ "other": "{0} 秒前",
+ },
+ },
+ },
+ "day_periods": {
+ "midnight": "真夜中",
+ "am": "午前",
+ "noon": "正午",
+ "pm": "午後",
+ "morning1": "朝",
+ "afternoon1": "昼",
+ "evening1": "夕方",
+ "night1": "夜",
+ "night2": "夜中",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/ko/custom.py b/pendulum/locales/ko/custom.py
index beac040..2c0e50c 100644
--- a/pendulum/locales/ko/custom.py
+++ b/pendulum/locales/ko/custom.py
@@ -1,22 +1,18 @@
-# -*- 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",
- },
-}
+"""
+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
index 3c81b0e..0f5a346 100644
--- a/pendulum/locales/ko/locale.py
+++ b/pendulum/locales/ko/locale.py
@@ -1,108 +1,105 @@
-# -*- 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,
-}
+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
index 154db42..637509a 100644
--- a/pendulum/locales/locale.py
+++ b/pendulum/locales/locale.py
@@ -1,104 +1,102 @@
-# -*- 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)
+from __future__ import annotations
+
+from importlib import import_module
+from pathlib import Path
+
+import re
+import sys
+from typing import Any, cast
+from typing import Dict
+
+if sys.version_info >= (3, 9):
+ from importlib import resources
+else:
+ import importlib_resources as resources
+
+
+class Locale:
+ """
+ Represent a specific locale.
+ """
+
+ _cache: dict[str, Locale] = {}
+
+ def __init__(self, locale: str, data: Any) -> None:
+ self._locale: str = locale
+ self._data: Any = data
+ self._key_cache: dict[str, str] = {}
+
+ @classmethod
+ def load(cls, locale: str | Locale) -> Locale:
+ if isinstance(locale, Locale):
+ return locale
+
+ locale = cls.normalize_locale(locale)
+ if locale in cls._cache:
+ return cls._cache[locale]
+
+ # Checking locale existence
+ actual_locale = locale
+ locale_path = cast(Path, resources.files(__package__).joinpath(actual_locale))
+ while not locale_path.exists():
+ if actual_locale == locale:
+ raise ValueError(f"Locale [{locale}] does not exist.")
+
+ actual_locale = actual_locale.split("_")[0]
+
+ m = import_module(f"pendulum.locales.{actual_locale}.locale")
+
+ cls._cache[locale] = cls(locale, m.locale)
+
+ return cls._cache[locale]
+
+ @classmethod
+ def normalize_locale(cls, locale: str) -> str:
+ m = re.match("([a-z]{2})[-_]([a-z]{2})", locale, re.I)
+ if m:
+ return f"{m.group(1).lower()}_{m.group(2).lower()}"
+ else:
+ return locale.lower()
+
+ def get(self, key: str, default: Any | None = None) -> Any:
+ if key in self._key_cache:
+ return self._key_cache[key]
+
+ parts = key.split(".")
+ try:
+ result = self._data[parts[0]]
+ for part in parts[1:]:
+ result = result[part]
+ except KeyError:
+ result = default
+
+ self._key_cache[key] = result
+
+ return self._key_cache[key]
+
+ def translation(self, key: str) -> Any:
+ return self.get(f"translations.{key}")
+
+ def plural(self, number: int) -> str:
+ return cast(str, self._data["plural"](number))
+
+ def ordinal(self, number: int) -> str:
+ return cast(str, self._data["ordinal"](number))
+
+ def ordinalize(self, number: int) -> str:
+ ordinal = self.get(f"custom.ordinal.{self.ordinal(number)}")
+
+ if not ordinal:
+ return f"{number}"
+
+ return f"{number}{ordinal}"
+
+ def match_translation(self, key: str, value: Any) -> dict[str, str] | None:
+ translations = self.translation(key)
+ if value not in translations.values():
+ return None
+
+ return cast(Dict[str, str], {v: k for k, v in translations.items()}[value])
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}('{self._locale}')"
diff --git a/pendulum/locales/lt/custom.py b/pendulum/locales/lt/custom.py
index addaaf8..6480c31 100644
--- a/pendulum/locales/lt/custom.py
+++ b/pendulum/locales/lt/custom.py
@@ -1,122 +1,118 @@
-# -*- 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",
- },
-}
+"""
+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
index 12451b6..fb017ef 100644
--- a/pendulum/locales/lt/locale.py
+++ b/pendulum/locales/lt/locale.py
@@ -1,258 +1,255 @@
-# -*- 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,
-}
+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/custom.py b/pendulum/locales/nb/custom.py
index 666f1b4..4c7cd6a 100644
--- a/pendulum/locales/nb/custom.py
+++ b/pendulum/locales/nb/custom.py
@@ -1,24 +1,20 @@
-# -*- 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",
- },
-}
+"""
+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
index 9ef9160..c8297a8 100644
--- a/pendulum/locales/nb/locale.py
+++ b/pendulum/locales/nb/locale.py
@@ -1,153 +1,150 @@
-# -*- 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,
-}
+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/custom.py b/pendulum/locales/nl/custom.py
index c957cda..2ca5a85 100644
--- a/pendulum/locales/nl/custom.py
+++ b/pendulum/locales/nl/custom.py
@@ -1,27 +1,23 @@
-# -*- 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",
- },
-}
+"""
+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
index 270f18e..cb1570d 100644
--- a/pendulum/locales/nl/locale.py
+++ b/pendulum/locales/nl/locale.py
@@ -1,137 +1,134 @@
-# -*- 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,
-}
+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/custom.py b/pendulum/locales/nn/custom.py
index 666f1b4..4c7cd6a 100644
--- a/pendulum/locales/nn/custom.py
+++ b/pendulum/locales/nn/custom.py
@@ -1,24 +1,20 @@
-# -*- 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",
- },
-}
+"""
+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
index 7236d0c..eb46e1d 100644
--- a/pendulum/locales/nn/locale.py
+++ b/pendulum/locales/nn/locale.py
@@ -1,144 +1,141 @@
-# -*- 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,
-}
+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/custom.py b/pendulum/locales/pl/custom.py
index dc20eb8..9741b74 100644
--- a/pendulum/locales/pl/custom.py
+++ b/pendulum/locales/pl/custom.py
@@ -1,25 +1,21 @@
-# -*- 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",
- },
-}
+"""
+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
index e603efb..bf6af10 100644
--- a/pendulum/locales/pl/locale.py
+++ b/pendulum/locales/pl/locale.py
@@ -1,282 +1,279 @@
-# -*- 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,
-}
+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/custom.py b/pendulum/locales/pt_br/custom.py
index 3cc3f0d..12aced7 100644
--- a/pendulum/locales/pt_br/custom.py
+++ b/pendulum/locales/pt_br/custom.py
@@ -1,22 +1,18 @@
-# -*- 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",
- },
-}
+"""
+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
index c70c671..742c41f 100644
--- a/pendulum/locales/pt_br/locale.py
+++ b/pendulum/locales/pt_br/locale.py
@@ -1,146 +1,143 @@
-# -*- 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,
-}
+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/custom.py b/pendulum/locales/ru/custom.py
index ed770c3..b4c89bb 100644
--- a/pendulum/locales/ru/custom.py
+++ b/pendulum/locales/ru/custom.py
@@ -1,24 +1,20 @@
-# -*- 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",
- },
-}
+"""
+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
index 8c7d53b..3736e0b 100644
--- a/pendulum/locales/ru/locale.py
+++ b/pendulum/locales/ru/locale.py
@@ -1,273 +1,270 @@
-# -*- 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,
-}
+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/sk/__init__.py b/pendulum/locales/sk/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/sk/__init__.py
diff --git a/pendulum/locales/sk/custom.py b/pendulum/locales/sk/custom.py
new file mode 100644
index 0000000..71afb15
--- /dev/null
+++ b/pendulum/locales/sk/custom.py
@@ -0,0 +1,20 @@
+"""
+sk custom locale file.
+"""
+
+translations = {
+ # Relative time
+ "ago": "pred {}",
+ "from_now": "o {}",
+ "after": "{0} po",
+ "before": "{0} pred",
+ # Date formats
+ "date_formats": {
+ "LTS": "HH:mm:ss",
+ "LT": "HH:mm",
+ "LLLL": "dddd, D. MMMM YYYY HH:mm",
+ "LLL": "D. MMMM YYYY HH:mm",
+ "LL": "D. MMMM YYYY",
+ "L": "DD.MM.YYYY",
+ },
+}
diff --git a/pendulum/locales/sk/locale.py b/pendulum/locales/sk/locale.py
new file mode 100644
index 0000000..8d3459f
--- /dev/null
+++ b/pendulum/locales/sk/locale.py
@@ -0,0 +1,266 @@
+from .custom import translations as custom_translations
+
+
+"""
+sk locale file.
+
+It has been generated automatically and must not be modified directly.
+"""
+
+
+locale = {
+ "plural": lambda n: "few"
+ if ((n == n and (n >= 2 and n <= 4)) and (0 == 0 and (0 == 0)))
+ else "many"
+ if (not (0 == 0 and (0 == 0)))
+ else "one"
+ if ((n == n and (n == 1)) and (0 == 0 and (0 == 0)))
+ else "other",
+ "ordinal": lambda n: "other",
+ "translations": {
+ "days": {
+ "abbreviated": {
+ 0: "ne",
+ 1: "po",
+ 2: "ut",
+ 3: "st",
+ 4: "št",
+ 5: "pi",
+ 6: "so",
+ },
+ "narrow": {
+ 0: "n",
+ 1: "p",
+ 2: "u",
+ 3: "s",
+ 4: "š",
+ 5: "p",
+ 6: "s",
+ },
+ "short": {
+ 0: "ne",
+ 1: "po",
+ 2: "ut",
+ 3: "st",
+ 4: "št",
+ 5: "pi",
+ 6: "so",
+ },
+ "wide": {
+ 0: "nedeľa",
+ 1: "pondelok",
+ 2: "utorok",
+ 3: "streda",
+ 4: "štvrtok",
+ 5: "piatok",
+ 6: "sobota",
+ },
+ },
+ "months": {
+ "abbreviated": {
+ 1: "jan",
+ 2: "feb",
+ 3: "mar",
+ 4: "apr",
+ 5: "máj",
+ 6: "jún",
+ 7: "júl",
+ 8: "aug",
+ 9: "sep",
+ 10: "okt",
+ 11: "nov",
+ 12: "dec",
+ },
+ "narrow": {
+ 1: "j",
+ 2: "f",
+ 3: "m",
+ 4: "a",
+ 5: "m",
+ 6: "j",
+ 7: "j",
+ 8: "a",
+ 9: "s",
+ 10: "o",
+ 11: "n",
+ 12: "d",
+ },
+ "wide": {
+ 1: "januára",
+ 2: "februára",
+ 3: "marca",
+ 4: "apríla",
+ 5: "mája",
+ 6: "júna",
+ 7: "júla",
+ 8: "augusta",
+ 9: "septembra",
+ 10: "októbra",
+ 11: "novembra",
+ 12: "decembra",
+ },
+ },
+ "units": {
+ "year": {
+ "one": "{0} rok",
+ "few": "{0} roky",
+ "many": "{0} roka",
+ "other": "{0} rokov",
+ },
+ "month": {
+ "one": "{0} mesiac",
+ "few": "{0} mesiace",
+ "many": "{0} mesiaca",
+ "other": "{0} mesiacov",
+ },
+ "week": {
+ "one": "{0} týždeň",
+ "few": "{0} týždne",
+ "many": "{0} týždňa",
+ "other": "{0} týždňov",
+ },
+ "day": {
+ "one": "{0} deň",
+ "few": "{0} dni",
+ "many": "{0} dňa",
+ "other": "{0} dní",
+ },
+ "hour": {
+ "one": "{0} hodina",
+ "few": "{0} hodiny",
+ "many": "{0} hodiny",
+ "other": "{0} hodín",
+ },
+ "minute": {
+ "one": "{0} minúta",
+ "few": "{0} minúty",
+ "many": "{0} minúty",
+ "other": "{0} minút",
+ },
+ "second": {
+ "one": "{0} sekunda",
+ "few": "{0} sekundy",
+ "many": "{0} sekundy",
+ "other": "{0} sekúnd",
+ },
+ "microsecond": {
+ "one": "{0} mikrosekunda",
+ "few": "{0} mikrosekundy",
+ "many": "{0} mikrosekundy",
+ "other": "{0} mikrosekúnd",
+ },
+ },
+ "relative": {
+ "year": {
+ "future": {
+ "other": "o {0} rokov",
+ "one": "o {0} rok",
+ "few": "o {0} roky",
+ "many": "o {0} roka",
+ },
+ "past": {
+ "other": "pred {0} rokmi",
+ "one": "pred {0} rokom",
+ "few": "pred {0} rokmi",
+ "many": "pred {0} roka",
+ },
+ },
+ "month": {
+ "future": {
+ "other": "o {0} mesiacov",
+ "one": "o {0} mesiac",
+ "few": "o {0} mesiace",
+ "many": "o {0} mesiaca",
+ },
+ "past": {
+ "other": "pred {0} mesiacmi",
+ "one": "pred {0} mesiacom",
+ "few": "pred {0} mesiacmi",
+ "many": "pred {0} mesiaca",
+ },
+ },
+ "week": {
+ "future": {
+ "other": "o {0} týždňov",
+ "one": "o {0} týždeň",
+ "few": "o {0} týždne",
+ "many": "o {0} týždňa",
+ },
+ "past": {
+ "other": "pred {0} týždňami",
+ "one": "pred {0} týždňom",
+ "few": "pred {0} týždňami",
+ "many": "pred {0} týždňa",
+ },
+ },
+ "day": {
+ "future": {
+ "other": "o {0} dní",
+ "one": "o {0} deň",
+ "few": "o {0} dni",
+ "many": "o {0} dňa",
+ },
+ "past": {
+ "other": "pred {0} dňami",
+ "one": "pred {0} dňom",
+ "few": "pred {0} dňami",
+ "many": "pred {0} dňa",
+ },
+ },
+ "hour": {
+ "future": {
+ "other": "o {0} hodín",
+ "one": "o {0} hodinu",
+ "few": "o {0} hodiny",
+ "many": "o {0} hodiny",
+ },
+ "past": {
+ "other": "pred {0} hodinami",
+ "one": "pred {0} hodinou",
+ "few": "pred {0} hodinami",
+ "many": "pred {0} hodinou",
+ },
+ },
+ "minute": {
+ "future": {
+ "other": "o {0} minút",
+ "one": "o {0} minútu",
+ "few": "o {0} minúty",
+ "many": "o {0} minúty",
+ },
+ "past": {
+ "other": "pred {0} minútami",
+ "one": "pred {0} minútou",
+ "few": "pred {0} minútami",
+ "many": "pred {0} minúty",
+ },
+ },
+ "second": {
+ "future": {
+ "other": "o {0} sekúnd",
+ "one": "o {0} sekundu",
+ "few": "o {0} sekundy",
+ "many": "o {0} sekundy",
+ },
+ "past": {
+ "other": "pred {0} sekundami",
+ "one": "pred {0} sekundou",
+ "few": "pred {0} sekundami",
+ "many": "pred {0} sekundy",
+ },
+ },
+ },
+ "day_periods": {
+ "midnight": "o polnoci",
+ "am": "AM",
+ "noon": "napoludnie",
+ "pm": "PM",
+ "morning1": "ráno",
+ "morning2": "dopoludnia",
+ "afternoon1": "popoludní",
+ "evening1": "večer",
+ "night1": "v noci",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/sv/__init__.py b/pendulum/locales/sv/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/locales/sv/__init__.py
diff --git a/pendulum/locales/sv/custom.py b/pendulum/locales/sv/custom.py
new file mode 100644
index 0000000..7158f4b
--- /dev/null
+++ b/pendulum/locales/sv/custom.py
@@ -0,0 +1,20 @@
+"""
+sv custom locale file.
+"""
+
+translations = {
+ # Relative time
+ "ago": "{} sedan",
+ "from_now": "från nu {}",
+ "after": "{0} efter",
+ "before": "{0} innan",
+ # Date formats
+ "date_formats": {
+ "LTS": "HH:mm:ss",
+ "LT": "HH:mm",
+ "L": "YYYY-MM-DD",
+ "LL": "D MMMM YYYY",
+ "LLL": "D MMMM YYYY, HH:mm",
+ "LLLL": "dddd, D MMMM YYYY, HH:mm",
+ },
+}
diff --git a/pendulum/locales/sv/locale.py b/pendulum/locales/sv/locale.py
new file mode 100644
index 0000000..5b74a6e
--- /dev/null
+++ b/pendulum/locales/sv/locale.py
@@ -0,0 +1,222 @@
+from .custom import translations as custom_translations
+
+
+"""
+sv locale file.
+
+It has been generated automatically and must not be modified directly.
+"""
+
+
+locale = {
+ "plural": lambda n: "one"
+ if ((n == n and (n == 1)) and (0 == 0 and (0 == 0)))
+ else "other",
+ "ordinal": lambda n: "one"
+ if (
+ ((n % 10) == (n % 10) and (((n % 10) == 1) or ((n % 10) == 2)))
+ and (not ((n % 100) == (n % 100) and (((n % 100) == 11) or ((n % 100) == 12))))
+ )
+ else "other",
+ "translations": {
+ "days": {
+ "abbreviated": {
+ 0: "sön",
+ 1: "mån",
+ 2: "tis",
+ 3: "ons",
+ 4: "tors",
+ 5: "fre",
+ 6: "lör",
+ },
+ "narrow": {
+ 0: "S",
+ 1: "M",
+ 2: "T",
+ 3: "O",
+ 4: "T",
+ 5: "F",
+ 6: "L",
+ },
+ "short": {
+ 0: "sö",
+ 1: "må",
+ 2: "ti",
+ 3: "on",
+ 4: "to",
+ 5: "fr",
+ 6: "lö",
+ },
+ "wide": {
+ 0: "söndag",
+ 1: "måndag",
+ 2: "tisdag",
+ 3: "onsdag",
+ 4: "torsdag",
+ 5: "fredag",
+ 6: "lördag",
+ },
+ },
+ "months": {
+ "abbreviated": {
+ 1: "jan.",
+ 2: "feb.",
+ 3: "mars",
+ 4: "apr.",
+ 5: "maj",
+ 6: "juni",
+ 7: "juli",
+ 8: "aug.",
+ 9: "sep.",
+ 10: "okt.",
+ 11: "nov.",
+ 12: "dec.",
+ },
+ "narrow": {
+ 1: "J",
+ 2: "F",
+ 3: "M",
+ 4: "A",
+ 5: "M",
+ 6: "J",
+ 7: "J",
+ 8: "A",
+ 9: "S",
+ 10: "O",
+ 11: "N",
+ 12: "D",
+ },
+ "wide": {
+ 1: "januari",
+ 2: "februari",
+ 3: "mars",
+ 4: "april",
+ 5: "maj",
+ 6: "juni",
+ 7: "juli",
+ 8: "augusti",
+ 9: "september",
+ 10: "oktober",
+ 11: "november",
+ 12: "december",
+ },
+ },
+ "units": {
+ "year": {
+ "one": "{0} år",
+ "other": "{0} år",
+ },
+ "month": {
+ "one": "{0} månad",
+ "other": "{0} månader",
+ },
+ "week": {
+ "one": "{0} vecka",
+ "other": "{0} veckor",
+ },
+ "day": {
+ "one": "{0} dygn",
+ "other": "{0} dygn",
+ },
+ "hour": {
+ "one": "{0} timme",
+ "other": "{0} timmar",
+ },
+ "minute": {
+ "one": "{0} minut",
+ "other": "{0} minuter",
+ },
+ "second": {
+ "one": "{0} sekund",
+ "other": "{0} sekunder",
+ },
+ "microsecond": {
+ "one": "{0} mikrosekund",
+ "other": "{0} mikrosekunder",
+ },
+ },
+ "relative": {
+ "year": {
+ "future": {
+ "other": "om {0} år",
+ "one": "om {0} år",
+ },
+ "past": {
+ "other": "för {0} år sedan",
+ "one": "för {0} år sedan",
+ },
+ },
+ "month": {
+ "future": {
+ "other": "om {0} månader",
+ "one": "om {0} månad",
+ },
+ "past": {
+ "other": "för {0} månader sedan",
+ "one": "för {0} månad sedan",
+ },
+ },
+ "week": {
+ "future": {
+ "other": "om {0} veckor",
+ "one": "om {0} vecka",
+ },
+ "past": {
+ "other": "för {0} veckor sedan",
+ "one": "för {0} vecka sedan",
+ },
+ },
+ "day": {
+ "future": {
+ "other": "om {0} dagar",
+ "one": "om {0} dag",
+ },
+ "past": {
+ "other": "för {0} dagar sedan",
+ "one": "för {0} dag sedan",
+ },
+ },
+ "hour": {
+ "future": {
+ "other": "om {0} timmar",
+ "one": "om {0} timme",
+ },
+ "past": {
+ "other": "för {0} timmar sedan",
+ "one": "för {0} timme sedan",
+ },
+ },
+ "minute": {
+ "future": {
+ "other": "om {0} minuter",
+ "one": "om {0} minut",
+ },
+ "past": {
+ "other": "för {0} minuter sedan",
+ "one": "för {0} minut sedan",
+ },
+ },
+ "second": {
+ "future": {
+ "other": "om {0} sekunder",
+ "one": "om {0} sekund",
+ },
+ "past": {
+ "other": "för {0} sekunder sedan",
+ "one": "för {0} sekund sedan",
+ },
+ },
+ },
+ "day_periods": {
+ "midnight": "midnatt",
+ "am": "fm",
+ "pm": "em",
+ "morning1": "på morgonen",
+ "morning2": "på förmiddagen",
+ "afternoon1": "på eftermiddagen",
+ "evening1": "på kvällen",
+ "night1": "på natten",
+ },
+ },
+ "custom": custom_translations,
+}
diff --git a/pendulum/locales/zh/custom.py b/pendulum/locales/zh/custom.py
index 7b35d66..69bc4ca 100644
--- a/pendulum/locales/zh/custom.py
+++ b/pendulum/locales/zh/custom.py
@@ -1,22 +1,18 @@
-# -*- 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",
- },
-}
+"""
+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
index ea04fc3..2df477f 100644
--- a/pendulum/locales/zh/locale.py
+++ b/pendulum/locales/zh/locale.py
@@ -1,116 +1,113 @@
-# -*- 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,
-}
+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
index 4c48b5a..e69de29 100644
--- a/pendulum/mixins/__init__.py
+++ b/pendulum/mixins/__init__.py
@@ -1 +0,0 @@
-# -*- coding: utf-8 -*-
diff --git a/pendulum/mixins/default.py b/pendulum/mixins/default.py
index bfb5912..59f985e 100644
--- a/pendulum/mixins/default.py
+++ b/pendulum/mixins/default.py
@@ -1,43 +1,36 @@
-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()
+from __future__ import annotations
+
+from pendulum.formatting import Formatter
+
+_formatter = Formatter()
+
+
+class FormattableMixin:
+ _formatter: Formatter = _formatter
+
+ def format(self, fmt: str, locale: str | None = None) -> str:
+ """
+ Formats the instance using the given format.
+
+ :param fmt: The format to use
+ :param locale: The locale to use
+ """
+ return self._formatter.format(self, fmt, locale)
+
+ def for_json(self) -> str:
+ """
+ Methods for automatic json serialization by simplejson.
+ """
+ return str(self)
+
+ def __format__(self, format_spec: str) -> str:
+ if len(format_spec) > 0:
+ if "%" in format_spec:
+ return self.strftime(format_spec)
+
+ return self.format(format_spec)
+
+ return str(self)
+
+ def __str__(self) -> str:
+ return self.isoformat()
diff --git a/pendulum/parser.py b/pendulum/parser.py
index 9b9e383..77900e2 100644
--- a/pendulum/parser.py
+++ b/pendulum/parser.py
@@ -1,121 +1,124 @@
-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
+from __future__ import annotations
+
+import datetime
+import typing as t
+
+import pendulum
+
+from pendulum.parsing import _Interval
+from pendulum.parsing import parse as base_parse
+from pendulum.tz.timezone import UTC
+
+if t.TYPE_CHECKING:
+ from pendulum.date import Date
+ from pendulum.datetime import DateTime
+ from pendulum.duration import Duration
+ from pendulum.interval import Interval
+ from pendulum.time import Time
+
+try:
+ from pendulum.parsing._iso8601 import Duration as CDuration
+except ImportError:
+ CDuration = None # type: ignore[misc, assignment]
+
+
+def parse(text: str, **options: t.Any) -> Date | Time | DateTime | Duration:
+ # Use the mock now value if it exists
+ options["now"] = options.get("now")
+
+ return _parse(text, **options)
+
+
+def _parse(text: str, **options: t.Any) -> Date | DateTime | Time | Duration | Interval:
+ """
+ Parses a string with the given options.
+
+ :param text: The string to parse.
+ """
+ # Handling special cases
+ if text == "now":
+ return pendulum.now()
+
+ parsed = base_parse(text, **options)
+
+ if isinstance(parsed, datetime.datetime):
+ return pendulum.datetime(
+ parsed.year,
+ parsed.month,
+ parsed.day,
+ parsed.hour,
+ parsed.minute,
+ parsed.second,
+ parsed.microsecond,
+ tz=parsed.tzinfo or options.get("tz", UTC),
+ )
+
+ if isinstance(parsed, datetime.date):
+ return pendulum.date(parsed.year, parsed.month, parsed.day)
+
+ if isinstance(parsed, datetime.time):
+ return pendulum.time(
+ parsed.hour, parsed.minute, parsed.second, parsed.microsecond
+ )
+
+ if isinstance(parsed, _Interval):
+ if parsed.duration is not None:
+ duration = parsed.duration
+
+ if parsed.start is not None:
+ dt = pendulum.instance(parsed.start, tz=options.get("tz", UTC))
+
+ return pendulum.interval(
+ dt,
+ dt.add(
+ years=duration.years,
+ months=duration.months,
+ weeks=duration.weeks,
+ days=duration.remaining_days,
+ hours=duration.hours,
+ minutes=duration.minutes,
+ seconds=duration.remaining_seconds,
+ microseconds=duration.microseconds,
+ ),
+ )
+
+ dt = pendulum.instance(
+ t.cast(datetime.datetime, parsed.end), tz=options.get("tz", UTC)
+ )
+
+ return pendulum.interval(
+ dt.subtract(
+ years=duration.years,
+ months=duration.months,
+ weeks=duration.weeks,
+ days=duration.remaining_days,
+ hours=duration.hours,
+ minutes=duration.minutes,
+ seconds=duration.remaining_seconds,
+ microseconds=duration.microseconds,
+ ),
+ dt,
+ )
+
+ return pendulum.interval(
+ pendulum.instance(
+ t.cast(datetime.datetime, parsed.start), tz=options.get("tz", UTC)
+ ),
+ pendulum.instance(
+ t.cast(datetime.datetime, parsed.end), tz=options.get("tz", UTC)
+ ),
+ )
+
+ if 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
index 400f119..0e64065 100644
--- a/pendulum/parsing/__init__.py
+++ b/pendulum/parsing/__init__.py
@@ -1,234 +1,233 @@
-import copy
-import os
-import re
-import struct
-
-from datetime import date
-from datetime import datetime
-from datetime import time
-
-from dateutil import parser
-
-from .exceptions import ParserError
-
-
-with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1"
-
-try:
- if not with_extensions or struct.calcsize("P") == 4:
- raise ImportError()
-
- from ._iso8601 import parse_iso8601
-except ImportError:
- from .iso8601 import parse_iso8601
-
-
-COMMON = re.compile(
- # Date (optional)
- "^"
- "(?P<date>"
- " (?P<classic>" # Classic date (YYYY-MM-DD)
- r" (?P<year>\d{4})" # Year
- " (?P<monthday>"
- r" (?P<monthsep>[/:])?(?P<month>\d{2})" # Month (optional)
- r" ((?P<daysep>[/:])?(?P<day>\d{2}))" # Day (optional)
- " )?"
- " )"
- ")?"
- # Time (optional)
- "(?P<time>"
- r" (?P<timesep>\ )?" # Separator (space)
- r" (?P<hour>\d{1,2}):(?P<minute>\d{1,2})?(?::(?P<second>\d{1,2}))?" # HH:mm:ss (optional mm and ss)
- # Subsecond part (optional)
- " (?P<subsecondsection>"
- " (?:[.|,])" # Subsecond separator (optional)
- r" (?P<subsecond>\d{1,9})" # Subsecond
- " )?"
- ")?"
- "$",
- re.VERBOSE,
-)
-
-
-DEFAULT_OPTIONS = {
- "day_first": False,
- "year_first": True,
- "strict": True,
- "exact": False,
- "now": None,
-}
-
-
-def parse(text, **options):
- """
- Parses a string with the given options.
-
- :param text: The string to parse.
- :type text: str
-
- :rtype: Parsed
- """
- _options = copy.copy(DEFAULT_OPTIONS)
- _options.update(options)
-
- return _normalize(_parse(text, **_options), **_options)
-
-
-def _normalize(parsed, **options):
- """
- Normalizes the parsed element.
-
- :param parsed: The parsed elements.
- :type parsed: Parsed
-
- :rtype: Parsed
- """
- if options.get("exact"):
- return parsed
-
- if isinstance(parsed, time):
- now = options["now"] or datetime.now()
-
- return datetime(
- now.year,
- now.month,
- now.day,
- parsed.hour,
- parsed.minute,
- parsed.second,
- parsed.microsecond,
- )
- elif isinstance(parsed, date) and not isinstance(parsed, datetime):
- return datetime(parsed.year, parsed.month, parsed.day)
-
- return parsed
-
-
-def _parse(text, **options):
- # Trying to parse ISO8601
- try:
- return parse_iso8601(text)
- except ValueError:
- pass
-
- try:
- return _parse_iso8601_interval(text)
- except ValueError:
- pass
-
- try:
- return _parse_common(text, **options)
- except ParserError:
- pass
-
- # We couldn't parse the string
- # so we fallback on the dateutil parser
- # If not strict
- if options.get("strict", True):
- raise ParserError("Unable to parse string [{}]".format(text))
-
- try:
- dt = parser.parse(
- text, dayfirst=options["day_first"], yearfirst=options["year_first"]
- )
- except ValueError:
- raise ParserError("Invalid date string: {}".format(text))
-
- return dt
-
-
-def _parse_common(text, **options):
- """
- Tries to parse the string as a common datetime format.
-
- :param text: The string to parse.
- :type text: str
-
- :rtype: dict or None
- """
- m = COMMON.match(text)
- has_date = False
- year = 0
- month = 1
- day = 1
-
- if not m:
- raise ParserError("Invalid datetime string")
-
- if m.group("date"):
- # A date has been specified
- has_date = True
-
- year = int(m.group("year"))
-
- if not m.group("monthday"):
- # No month and day
- month = 1
- day = 1
- else:
- if options["day_first"]:
- month = int(m.group("day"))
- day = int(m.group("month"))
- else:
- month = int(m.group("month"))
- day = int(m.group("day"))
-
- if not m.group("time"):
- return date(year, month, day)
-
- # Grabbing hh:mm:ss
- hour = int(m.group("hour"))
-
- minute = int(m.group("minute"))
-
- if m.group("second"):
- second = int(m.group("second"))
- else:
- second = 0
-
- # Grabbing subseconds, if any
- microsecond = 0
- if m.group("subsecondsection"):
- # Limiting to 6 chars
- subsecond = m.group("subsecond")[:6]
-
- microsecond = int("{:0<6}".format(subsecond))
-
- if has_date:
- return datetime(year, month, day, hour, minute, second, microsecond)
-
- return time(hour, minute, second, microsecond)
-
-
-class _Interval:
- """
- Special class to handle ISO 8601 intervals
- """
-
- def __init__(self, start=None, end=None, duration=None):
- self.start = start
- self.end = end
- self.duration = duration
-
-
-def _parse_iso8601_interval(text):
- if "/" not in text:
- raise ParserError("Invalid interval")
-
- first, last = text.split("/")
- start = end = duration = None
-
- if first[0] == "P":
- # duration/end
- duration = parse_iso8601(first)
- end = parse_iso8601(last)
- elif last[0] == "P":
- # start/duration
- start = parse_iso8601(first)
- duration = parse_iso8601(last)
- else:
- # start/end
- start = parse_iso8601(first)
- end = parse_iso8601(last)
-
- return _Interval(start, end, duration)
+from __future__ import annotations
+
+import contextlib
+import copy
+import os
+import re
+import struct
+
+from datetime import date
+from datetime import datetime
+from datetime import time
+from typing import Any
+from typing import Optional
+from typing import cast
+
+from dateutil import parser
+
+from pendulum.parsing.exceptions import ParserError
+
+with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1"
+
+try:
+ if not with_extensions or struct.calcsize("P") == 4:
+ raise ImportError()
+
+ from pendulum.parsing._iso8601 import Duration
+ from pendulum.parsing._iso8601 import parse_iso8601
+except ImportError:
+ from pendulum.duration import Duration # type: ignore[misc]
+ from pendulum.parsing.iso8601 import parse_iso8601 # type: ignore[misc]
+
+COMMON = re.compile(
+ # Date (optional) # noqa: E800
+ "^"
+ "(?P<date>"
+ " (?P<classic>" # Classic date (YYYY-MM-DD)
+ r" (?P<year>\d{4})" # Year
+ " (?P<monthday>"
+ r" (?P<monthsep>[/:])?(?P<month>\d{2})" # Month (optional)
+ r" ((?P<daysep>[/:])?(?P<day>\d{2}))" # Day (optional)
+ " )?"
+ " )"
+ ")?"
+ # Time (optional) # noqa: E800
+ "(?P<time>"
+ r" (?P<timesep>\ )?" # Separator (space)
+ r" (?P<hour>\d{1,2}):(?P<minute>\d{1,2})?(?::(?P<second>\d{1,2}))?" # HH:mm:ss (optional mm and ss)
+ # Subsecond part (optional)
+ " (?P<subsecondsection>"
+ " (?:[.|,])" # Subsecond separator (optional)
+ r" (?P<subsecond>\d{1,9})" # Subsecond
+ " )?"
+ ")?"
+ "$",
+ re.VERBOSE,
+)
+
+DEFAULT_OPTIONS = {
+ "day_first": False,
+ "year_first": True,
+ "strict": True,
+ "exact": False,
+ "now": None,
+}
+
+
+def parse(text: str, **options: Any) -> datetime | date | time | _Interval | Duration:
+ """
+ Parses a string with the given options.
+
+ :param text: The string to parse.
+ """
+ _options: dict[str, Any] = copy.copy(DEFAULT_OPTIONS)
+ _options.update(options)
+
+ return _normalize(_parse(text, **_options), **_options)
+
+
+def _normalize(
+ parsed: datetime | date | time | _Interval | Duration, **options: Any
+) -> datetime | date | time | _Interval | Duration:
+ """
+ Normalizes the parsed element.
+
+ :param parsed: The parsed elements.
+ """
+ if options.get("exact"):
+ return parsed
+
+ if isinstance(parsed, time):
+ now = cast(Optional[datetime], options["now"]) or datetime.now()
+
+ return datetime(
+ now.year,
+ now.month,
+ now.day,
+ parsed.hour,
+ parsed.minute,
+ parsed.second,
+ parsed.microsecond,
+ )
+ elif isinstance(parsed, date) and not isinstance(parsed, datetime):
+ return datetime(parsed.year, parsed.month, parsed.day)
+
+ return parsed
+
+
+def _parse(text: str, **options: Any) -> datetime | date | time | _Interval | Duration:
+ # Trying to parse ISO8601
+ with contextlib.suppress(ValueError):
+ return parse_iso8601(text)
+
+ with contextlib.suppress(ValueError):
+ return _parse_iso8601_interval(text)
+
+ with contextlib.suppress(ParserError):
+ return _parse_common(text, **options)
+
+ # We couldn't parse the string
+ # so we fallback on the dateutil parser
+ # If not strict
+ if options.get("strict", True):
+ raise ParserError(f"Unable to parse string [{text}]")
+
+ try:
+ dt = parser.parse(
+ text, dayfirst=options["day_first"], yearfirst=options["year_first"]
+ )
+ except ValueError:
+ raise ParserError(f"Invalid date string: {text}")
+
+ return dt
+
+
+def _parse_common(text: str, **options: Any) -> datetime | date | time:
+ """
+ Tries to parse the string as a common datetime format.
+
+ :param text: The string to parse.
+ """
+ m = COMMON.match(text)
+ has_date = False
+ year = 0
+ month = 1
+ day = 1
+
+ if not m:
+ raise ParserError("Invalid datetime string")
+
+ if m.group("date"):
+ # A date has been specified
+ has_date = True
+
+ year = int(m.group("year"))
+
+ if not m.group("monthday"):
+ # No month and day
+ month = 1
+ day = 1
+ else:
+ if options["day_first"]:
+ month = int(m.group("day"))
+ day = int(m.group("month"))
+ else:
+ month = int(m.group("month"))
+ day = int(m.group("day"))
+
+ if not m.group("time"):
+ return date(year, month, day)
+
+ # Grabbing hh:mm:ss
+ hour = int(m.group("hour"))
+
+ minute = int(m.group("minute"))
+
+ if m.group("second"):
+ second = int(m.group("second"))
+ else:
+ second = 0
+
+ # Grabbing subseconds, if any
+ microsecond = 0
+ if m.group("subsecondsection"):
+ # Limiting to 6 chars
+ subsecond = m.group("subsecond")[:6]
+
+ microsecond = int(f"{subsecond:0<6}")
+
+ if has_date:
+ return datetime(year, month, day, hour, minute, second, microsecond)
+
+ return time(hour, minute, second, microsecond)
+
+
+class _Interval:
+ """
+ Special class to handle ISO 8601 intervals
+ """
+
+ def __init__(
+ self,
+ start: datetime | None = None,
+ end: datetime | None = None,
+ duration: Duration | None = None,
+ ) -> None:
+ self.start = start
+ self.end = end
+ self.duration = duration
+
+
+def _parse_iso8601_interval(text: str) -> _Interval:
+ if "/" not in text:
+ raise ParserError("Invalid interval")
+
+ first, last = text.split("/")
+ start = end = duration = None
+
+ if first[0] == "P":
+ # duration/end
+ duration = parse_iso8601(first)
+ end = parse_iso8601(last)
+ elif last[0] == "P":
+ # start/duration
+ start = parse_iso8601(first)
+ duration = parse_iso8601(last)
+ else:
+ # start/end
+ start = parse_iso8601(first)
+ end = parse_iso8601(last)
+
+ return _Interval(
+ cast(datetime, start), cast(datetime, end), cast(Duration, duration)
+ )
diff --git a/pendulum/parsing/_iso8601.c b/pendulum/parsing/_iso8601.c
index 2e14e4b..1322423 100644
--- a/pendulum/parsing/_iso8601.c
+++ b/pendulum/parsing/_iso8601.c
@@ -1,1371 +1,1361 @@
-/* ------------------------------------------------------------------------- */
-
-#include <Python.h>
-#include <datetime.h>
-#include <structmember.h>
-#include <math.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <stdint.h>
-
-#ifndef PyVarObject_HEAD_INIT
-#define PyVarObject_HEAD_INIT(type, size) PyObject_HEAD_INIT(type) size,
-#endif
-
-
-/* ------------------------------------------------------------------------- */
-
-#define EPOCH_YEAR 1970
-
-#define DAYS_PER_N_YEAR 365
-#define DAYS_PER_L_YEAR 366
-
-#define USECS_PER_SEC 1000000
-
-#define SECS_PER_MIN 60
-#define SECS_PER_HOUR (60 * SECS_PER_MIN)
-#define SECS_PER_DAY (SECS_PER_HOUR * 24)
-
-// 400-year chunks always have 146097 days (20871 weeks).
-#define DAYS_PER_400_YEARS 146097L
-#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)SECS_PER_DAY)
-
-// The number of seconds in an aligned 100-year chunk, for those that
-// do not begin with a leap year and those that do respectively.
-const int64_t SECS_PER_100_YEARS[2] = {
- (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY,
- (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * DAYS_PER_L_YEAR) * SECS_PER_DAY
-};
-
-// The number of seconds in an aligned 4-year chunk, for those that
-// do not begin with a leap year and those that do respectively.
-const int32_t SECS_PER_4_YEARS[2] = {
- (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY,
- (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY
-};
-
-// The number of seconds in non-leap and leap years respectively.
-const int32_t SECS_PER_YEAR[2] = {
- DAYS_PER_N_YEAR * SECS_PER_DAY,
- DAYS_PER_L_YEAR * SECS_PER_DAY
-};
-
-#define MONTHS_PER_YEAR 12
-
-// The month lengths in non-leap and leap years respectively.
-const int32_t DAYS_PER_MONTHS[2][13] = {
- {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
- {-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
-};
-
-// The day offsets of the beginning of each (1-based) month in non-leap
-// and leap years respectively.
-// For example, in a leap year there are 335 days before December.
-const int32_t MONTHS_OFFSETS[2][14] = {
- {-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365},
- {-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}
-};
-
-const int DAY_OF_WEEK_TABLE[12] = {
- 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4
-};
-
-#define TM_SUNDAY 0
-#define TM_MONDAY 1
-#define TM_TUESDAY 2
-#define TM_WEDNESDAY 3
-#define TM_THURSDAY 4
-#define TM_FRIDAY 5
-#define TM_SATURDAY 6
-
-#define TM_JANUARY 0
-#define TM_FEBRUARY 1
-#define TM_MARCH 2
-#define TM_APRIL 3
-#define TM_MAY 4
-#define TM_JUNE 5
-#define TM_JULY 6
-#define TM_AUGUST 7
-#define TM_SEPTEMBER 8
-#define TM_OCTOBER 9
-#define TM_NOVEMBER 10
-#define TM_DECEMBER 11
-
-// Parsing errors
-const int PARSER_INVALID_ISO8601 = 0;
-const int PARSER_INVALID_DATE = 1;
-const int PARSER_INVALID_TIME = 2;
-const int PARSER_INVALID_WEEK_DATE = 3;
-const int PARSER_INVALID_WEEK_NUMBER = 4;
-const int PARSER_INVALID_WEEKDAY_NUMBER = 5;
-const int PARSER_INVALID_ORDINAL_DAY_FOR_YEAR = 6;
-const int PARSER_INVALID_MONTH_OR_DAY = 7;
-const int PARSER_INVALID_MONTH = 8;
-const int PARSER_INVALID_DAY_FOR_MONTH = 9;
-const int PARSER_INVALID_HOUR = 10;
-const int PARSER_INVALID_MINUTE = 11;
-const int PARSER_INVALID_SECOND = 12;
-const int PARSER_INVALID_SUBSECOND = 13;
-const int PARSER_INVALID_TZ_OFFSET = 14;
-const int PARSER_INVALID_DURATION = 15;
-const int PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED = 16;
-
-const char PARSER_ERRORS[17][80] = {
- "Invalid ISO 8601 string",
- "Invalid date",
- "Invalid time",
- "Invalid week date",
- "Invalid week number",
- "Invalid weekday number",
- "Invalid ordinal day for year",
- "Invalid month and/or day",
- "Invalid month",
- "Invalid day for month",
- "Invalid hour",
- "Invalid minute",
- "Invalid second",
- "Invalid subsecond",
- "Invalid timezone offset",
- "Invalid duration",
- "Float years and months are not supported"
-};
-
-/* ------------------------------------------------------------------------- */
-
-
-int p(int y) {
- return y + y/4 - y/100 + y/400;
-}
-
-int is_leap(int year) {
- return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
-}
-
-int week_day(int year, int month, int day) {
- int y;
- int w;
-
- y = year - (month < 3);
-
- w = (p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7;
-
- if (!w) {
- w = 7;
- }
-
- return w;
-}
-
-int days_in_year(int year) {
- if (is_leap(year)) {
- return DAYS_PER_L_YEAR;
- }
-
- return DAYS_PER_N_YEAR;
-}
-
-int is_long_year(int year) {
- return (p(year) % 7 == 4) || (p(year - 1) % 7 == 3);
-}
-
-
-/* ------------------------ Custom Types ------------------------------- */
-
-
-/*
- * class FixedOffset(tzinfo):
- */
-typedef struct {
- PyObject_HEAD
- int offset;
- char *tzname;
-} FixedOffset;
-
-/*
- * def __init__(self, offset):
- * self.offset = offset
-*/
-static int FixedOffset_init(FixedOffset *self, PyObject *args, PyObject *kwargs) {
- int offset;
- char *tzname = NULL;
-
- static char *kwlist[] = {"offset", "tzname", NULL};
-
- if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|s", kwlist, &offset, &tzname))
- return -1;
-
- self->offset = offset;
- self->tzname = tzname;
-
- return 0;
-}
-
-/*
- * def utcoffset(self, dt):
- * return timedelta(seconds=self.offset * 60)
- */
-static PyObject *FixedOffset_utcoffset(FixedOffset *self, PyObject *args) {
- return PyDelta_FromDSU(0, self->offset, 0);
-}
-
-/*
- * def dst(self, dt):
- * return timedelta(seconds=self.offset * 60)
- */
-static PyObject *FixedOffset_dst(FixedOffset *self, PyObject *args) {
- return PyDelta_FromDSU(0, self->offset, 0);
-}
-
-/*
- * def tzname(self, dt):
- * sign = '+'
- * if self.offset < 0:
- * sign = '-'
- * return "%s%d:%d" % (sign, self.offset / 60, self.offset % 60)
- */
-static PyObject *FixedOffset_tzname(FixedOffset *self, PyObject *args) {
- if (self->tzname != NULL) {
- return PyUnicode_FromString(self->tzname);
- }
-
- char tzname_[7] = {0};
- char sign = '+';
- int offset = self->offset;
-
- if (offset < 0) {
- sign = '-';
- offset *= -1;
- }
-
- sprintf(
- tzname_,
- "%c%02d:%02d",
- sign,
- offset / SECS_PER_HOUR,
- offset / SECS_PER_MIN % SECS_PER_MIN
- );
-
- return PyUnicode_FromString(tzname_);
-}
-
-/*
- * def __repr__(self):
- * return self.tzname()
- */
-static PyObject *FixedOffset_repr(FixedOffset *self) {
- return FixedOffset_tzname(self, NULL);
-}
-
-/*
- * Class member / class attributes
- */
-static PyMemberDef FixedOffset_members[] = {
- {"offset", T_INT, offsetof(FixedOffset, offset), 0, "UTC offset"},
- {NULL}
-};
-
-/*
- * Class methods
- */
-static PyMethodDef FixedOffset_methods[] = {
- {"utcoffset", (PyCFunction)FixedOffset_utcoffset, METH_VARARGS, ""},
- {"dst", (PyCFunction)FixedOffset_dst, METH_VARARGS, ""},
- {"tzname", (PyCFunction)FixedOffset_tzname, METH_VARARGS, ""},
- {NULL}
-};
-
-static PyTypeObject FixedOffset_type = {
- PyVarObject_HEAD_INIT(NULL, 0)
- "FixedOffset_type", /* tp_name */
- sizeof(FixedOffset), /* tp_basicsize */
- 0, /* tp_itemsize */
- 0, /* tp_dealloc */
- 0, /* tp_print */
- 0, /* tp_getattr */
- 0, /* tp_setattr */
- 0, /* tp_as_async */
- (reprfunc)FixedOffset_repr, /* tp_repr */
- 0, /* tp_as_number */
- 0, /* tp_as_sequence */
- 0, /* tp_as_mapping */
- 0, /* tp_hash */
- 0, /* tp_call */
- (reprfunc)FixedOffset_repr, /* tp_str */
- 0, /* tp_getattro */
- 0, /* tp_setattro */
- 0, /* tp_as_buffer */
- Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */
- "TZInfo with fixed offset", /* tp_doc */
-};
-
-/*
- * Instantiate new FixedOffset_type object
- * Skip overhead of calling PyObject_New and PyObject_Init.
- * Directly allocate object.
- */
-static PyObject *new_fixed_offset_ex(int offset, char *name, PyTypeObject *type) {
- FixedOffset *self = (FixedOffset *) (type->tp_alloc(type, 0));
-
- if (self != NULL)
- self->offset = offset;
- self->tzname = name;
-
- return (PyObject *) self;
-}
-
-#define new_fixed_offset(offset, name) new_fixed_offset_ex(offset, name, &FixedOffset_type)
-
-
-/*
- * class Duration():
- */
-typedef struct {
- PyObject_HEAD
- int years;
- int months;
- int weeks;
- int days;
- int hours;
- int minutes;
- int seconds;
- int microseconds;
-} Duration;
-
-/*
- * def __init__(self, years, months, days, hours, minutes, seconds, microseconds):
- * self.years = years
- * # ...
-*/
-static int Duration_init(Duration *self, PyObject *args, PyObject *kwargs) {
- int years;
- int months;
- int weeks;
- int days;
- int hours;
- int minutes;
- int seconds;
- int microseconds;
-
- if (!PyArg_ParseTuple(args, "iiiiiiii", &years, &months, &weeks, &days, &hours, &minutes, &seconds, &microseconds))
- return -1;
-
- self->years = years;
- self->months = months;
- self->weeks = weeks;
- self->days = days;
- self->hours = hours;
- self->minutes = minutes;
- self->seconds = seconds;
- self->microseconds = microseconds;
-
- return 0;
-}
-
-/*
- * def __repr__(self):
- * return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format(
- * self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds
- * )
- */
-static PyObject *Duration_repr(Duration *self) {
- char repr[82] = {0};
-
- sprintf(
- repr,
- "%d years %d months %d weeks %d days %d hours %d minutes %d seconds %d microseconds",
- self->years,
- self->months,
- self->weeks,
- self->days,
- self->hours,
- self->minutes,
- self->seconds,
- self->microseconds
- );
-
- return PyUnicode_FromString(repr);
-}
-
-/*
- * Instantiate new Duration_type object
- * Skip overhead of calling PyObject_New and PyObject_Init.
- * Directly allocate object.
- */
-static PyObject *new_duration_ex(int years, int months, int weeks, int days, int hours, int minutes, int seconds, int microseconds, PyTypeObject *type) {
- Duration *self = (Duration *) (type->tp_alloc(type, 0));
-
- if (self != NULL) {
- self->years = years;
- self->months = months;
- self->weeks = weeks;
- self->days = days;
- self->hours = hours;
- self->minutes = minutes;
- self->seconds = seconds;
- self->microseconds = microseconds;
- }
-
- return (PyObject *) self;
-}
-
-/*
- * Class member / class attributes
- */
-static PyMemberDef Duration_members[] = {
- {"years", T_INT, offsetof(Duration, years), 0, "years in duration"},
- {"months", T_INT, offsetof(Duration, months), 0, "months in duration"},
- {"weeks", T_INT, offsetof(Duration, weeks), 0, "weeks in duration"},
- {"days", T_INT, offsetof(Duration, days), 0, "days in duration"},
- {"remaining_days", T_INT, offsetof(Duration, days), 0, "days in duration"},
- {"hours", T_INT, offsetof(Duration, hours), 0, "hours in duration"},
- {"minutes", T_INT, offsetof(Duration, minutes), 0, "minutes in duration"},
- {"seconds", T_INT, offsetof(Duration, seconds), 0, "seconds in duration"},
- {"remaining_seconds", T_INT, offsetof(Duration, seconds), 0, "seconds in duration"},
- {"microseconds", T_INT, offsetof(Duration, microseconds), 0, "microseconds in duration"},
- {NULL}
-};
-
-static PyTypeObject Duration_type = {
- PyVarObject_HEAD_INIT(NULL, 0)
- "Duration", /* tp_name */
- sizeof(Duration), /* tp_basicsize */
- 0, /* tp_itemsize */
- 0, /* tp_dealloc */
- 0, /* tp_print */
- 0, /* tp_getattr */
- 0, /* tp_setattr */
- 0, /* tp_as_async */
- (reprfunc)Duration_repr, /* tp_repr */
- 0, /* tp_as_number */
- 0, /* tp_as_sequence */
- 0, /* tp_as_mapping */
- 0, /* tp_hash */
- 0, /* tp_call */
- (reprfunc)Duration_repr, /* tp_str */
- 0, /* tp_getattro */
- 0, /* tp_setattro */
- 0, /* tp_as_buffer */
- Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */
- "Duration", /* tp_doc */
-};
-
-#define new_duration(years, months, weeks, days, hours, minutes, seconds, microseconds) new_duration_ex(years, months, weeks, days, hours, minutes, seconds, microseconds, &Duration_type)
-
-typedef struct {
- int is_date;
- int is_time;
- int is_datetime;
- int is_duration;
- int is_period;
- int ambiguous;
- int year;
- int month;
- int day;
- int hour;
- int minute;
- int second;
- int microsecond;
- int offset;
- int has_offset;
- char *tzname;
- int years;
- int months;
- int weeks;
- int days;
- int hours;
- int minutes;
- int seconds;
- int microseconds;
- int error;
-} Parsed;
-
-
-Parsed* new_parsed() {
- Parsed *parsed;
-
- if((parsed = malloc(sizeof *parsed)) != NULL) {
- parsed->is_date = 0;
- parsed->is_time = 0;
- parsed->is_datetime = 0;
- parsed->is_duration = 0;
- parsed->is_period = 0;
-
- parsed->ambiguous = 0;
- parsed->year = 0;
- parsed->month = 1;
- parsed->day = 1;
- parsed->hour = 0;
- parsed->minute = 0;
- parsed->second = 0;
- parsed->microsecond = 0;
- parsed->offset = 0;
- parsed->has_offset = 0;
- parsed->tzname = NULL;
-
- parsed->years = 0;
- parsed->months = 0;
- parsed->weeks = 0;
- parsed->days = 0;
- parsed->hours = 0;
- parsed->minutes = 0;
- parsed->seconds = 0;
- parsed->microseconds = 0;
-
- parsed->error = -1;
- }
-
- return parsed;
-}
-
-
-/* -------------------------- Functions --------------------------*/
-
-Parsed* _parse_iso8601_datetime(char *str, Parsed *parsed) {
- char* c;
- int monthday = 0;
- int week = 0;
- int weekday = 1;
- int ordinal;
- int tz_sign = 0;
- int leap = 0;
- int separators = 0;
- int time = 0;
- int has_hour = 0;
- int i;
- int j;
-
- // Assuming date only for now
- parsed->is_date = 1;
-
- c = str;
-
- for (i = 0; i < 4; i++) {
- if (*c >= '0' && *c <= '9') {
- parsed->year = 10 * parsed->year + *c++ - '0';
- } else {
- parsed->error = PARSER_INVALID_ISO8601;
-
- return NULL;
- }
- }
-
- leap = is_leap(parsed->year);
-
- // Optional separator
- if (*c == '-') {
- separators++;
- c++;
- }
-
- // Checking for week dates
- if (*c == 'W') {
- c++;
-
- i = 0;
- while (*c != '\0' && *c != ' ' && *c != 'T') {
- if (*c == '-') {
- separators++;
- c++;
- continue;
- }
-
- week = 10 * week + *c++ - '0';
-
- i++;
- }
-
- switch (i) {
- case 2:
- // Only week number
- break;
- case 3:
- // Week with weekday
- if (!(separators == 0 || separators == 2)) {
- // We should have 2 or no separator
- parsed->error = PARSER_INVALID_WEEK_DATE;
-
- return NULL;
- }
-
- weekday = week % 10;
- week /= 10;
-
- break;
- default:
- // Any other case is wrong
- parsed->error = PARSER_INVALID_WEEK_DATE;
-
- return NULL;
- }
-
- // Checks
- if (week > 53 || (week > 52 && !is_long_year(parsed->year))) {
- parsed->error = PARSER_INVALID_WEEK_NUMBER;
-
- return NULL;
- }
-
- if (weekday > 7) {
- parsed->error = PARSER_INVALID_WEEKDAY_NUMBER;
-
- return NULL;
- }
-
- // Calculating ordinal day
- ordinal = week * 7 + weekday - (week_day(parsed->year, 1, 4) + 3);
-
- if (ordinal < 1) {
- // Previous year
- ordinal += days_in_year(parsed->year - 1);
- parsed->year -= 1;
- leap = is_leap(parsed->year);
- }
-
- if (ordinal > days_in_year(parsed->year)) {
- // Next year
- ordinal -= days_in_year(parsed->year);
- parsed->year += 1;
- leap = is_leap(parsed->year);
- }
-
- for (j = 1; j < 14; j++) {
- if (ordinal <= MONTHS_OFFSETS[leap][j]) {
- parsed->day = ordinal - MONTHS_OFFSETS[leap][j - 1];
- parsed->month = j - 1;
-
- break;
- }
- }
- } else {
- // At this point we need to check the number
- // of characters until the end of the date part
- // (or the end of the string).
- //
- // If two, we have only a month if there is a separator, it may be a time otherwise.
- // If three, we have an ordinal date.
- // If four, we have a complete date
- i = 0;
- while (*c != '\0' && *c != ' ' && *c != 'T') {
- if (*c == '-') {
- separators++;
- c++;
- continue;
- }
-
- if (!(*c >= '0' && *c <='9')) {
- parsed->error = PARSER_INVALID_DATE;
-
- return NULL;
- }
-
- monthday = 10 * monthday + *c++ - '0';
-
- i++;
- }
-
- switch (i) {
- case 0:
- // No month/day specified (only a year)
- break;
- case 2:
- if (!separators) {
- // The date looks like 201207
- // which is invalid for a date
- // But it might be a time in the form hhmmss
- parsed->ambiguous = 1;
- } else if (separators > 1) {
- parsed->error = PARSER_INVALID_DATE;
-
- return NULL;
- }
-
- parsed->month = monthday;
- break;
- case 3:
- // Ordinal day
- if (separators > 1) {
- parsed->error = PARSER_INVALID_DATE;
-
- return NULL;
- }
-
- if (monthday < 1 || monthday > MONTHS_OFFSETS[leap][13]) {
- parsed->error = PARSER_INVALID_ORDINAL_DAY_FOR_YEAR;
-
- return NULL;
- }
-
- for (j = 1; j < 14; j++) {
- if (monthday <= MONTHS_OFFSETS[leap][j]) {
- parsed->day = monthday - MONTHS_OFFSETS[leap][j - 1];
- parsed->month = j - 1;
-
- break;
- }
- }
-
- break;
- case 4:
- // Month and day
- parsed->month = monthday / 100;
- parsed->day = monthday % 100;
-
- break;
- default:
- parsed->error = PARSER_INVALID_MONTH_OR_DAY;
-
- return NULL;
- }
- }
-
- // Checks
- if (separators && !monthday && !week) {
- parsed->error = PARSER_INVALID_DATE;
-
- return NULL;
- }
-
- if (parsed->month > 12) {
- parsed->error = PARSER_INVALID_MONTH;
-
- return NULL;
- }
-
- if (parsed->day > DAYS_PER_MONTHS[leap][parsed->month]) {
- parsed->error = PARSER_INVALID_DAY_FOR_MONTH;
-
- return NULL;
- }
-
- separators = 0;
- if (*c == 'T' || *c == ' ') {
- if (parsed->ambiguous) {
- parsed->error = PARSER_INVALID_DATE;
-
- return NULL;
- }
-
- // We have time so we have a datetime
- parsed->is_datetime = 1;
- parsed->is_date = 0;
-
- c++;
-
- // Grabbing time information
- i = 0;
- while (*c != '\0' && *c != '.' && *c != ',' && *c != 'Z' && *c != '+' && *c != '-') {
- if (*c == ':') {
- separators++;
- c++;
- continue;
- }
-
- if (!(*c >= '0' && *c <='9')) {
- parsed->error = PARSER_INVALID_TIME;
-
- return NULL;
- }
-
- time = 10 * time + *c++ - '0';
- i++;
- }
-
- switch (i) {
- case 2:
- // Hours only
- if (separators > 0) {
- // Extraneous separators
- parsed->error = PARSER_INVALID_TIME;
-
- return NULL;
- }
-
- parsed->hour = time;
- has_hour = 1;
- break;
- case 4:
- // Hours and minutes
- if (separators > 1) {
- // Extraneous separators
- parsed->error = PARSER_INVALID_TIME;
-
- return NULL;
- }
-
- parsed->hour = time / 100;
- parsed->minute = time % 100;
- has_hour = 1;
- break;
- case 6:
- // Hours, minutes and seconds
- if (!(separators == 0 || separators == 2)) {
- // We should have either two separators or none
- parsed->error = PARSER_INVALID_TIME;
-
- return NULL;
- }
-
- parsed->hour = time / 10000;
- parsed->minute = time / 100 % 100;
- parsed->second = time % 100;
- has_hour = 1;
- break;
- default:
- // Any other case is wrong
- parsed->error = PARSER_INVALID_TIME;
-
- return NULL;
- }
-
- // Checks
- if (parsed->hour > 23) {
- parsed->error = PARSER_INVALID_HOUR;
-
- return NULL;
- }
-
- if (parsed->minute > 59) {
- parsed->error = PARSER_INVALID_MINUTE;
-
- return NULL;
- }
-
- if (parsed->second > 59) {
- parsed->error = PARSER_INVALID_SECOND;
-
- return NULL;
- }
-
- // Subsecond
- if (*c == '.' || *c == ',') {
- c++;
-
- time = 0;
- i = 0;
- while (*c != '\0' && *c != 'Z' && *c != '+' && *c != '-') {
- if (!(*c >= '0' && *c <='9')) {
- parsed->error = PARSER_INVALID_SUBSECOND;
-
- return NULL;
- }
-
- time = 10 * time + *c++ - '0';
- i++;
- }
-
- // adjust to microseconds
- if (i > 6) {
- parsed->microsecond = time / pow(10, i - 6);
- } else if (i <= 6) {
- parsed->microsecond = time * pow(10, 6 - i);
- }
- }
-
- // Timezone
- if (*c == 'Z') {
- parsed->has_offset = 1;
- parsed->tzname = "UTC";
- c++;
- } else if (*c == '+' || *c == '-') {
- tz_sign = 1;
- if (*c == '-') {
- tz_sign = -1;
- }
-
- parsed->has_offset = 1;
- c++;
-
- i = 0;
- time = 0;
- separators = 0;
- while (*c != '\0') {
- if (*c == ':') {
- separators++;
- c++;
- continue;
- }
-
- if (!(*c >= '0' && *c <= '9')) {
- parsed->error = PARSER_INVALID_TZ_OFFSET;
-
- return NULL;
- }
-
- time = 10 * time + *c++ - '0';
- i++;
- }
-
- switch (i) {
- case 2:
- // hh Format
- if (separators) {
- // Extraneous separators
- parsed->error = PARSER_INVALID_TZ_OFFSET;
-
- return NULL;
- }
-
- parsed->offset = tz_sign * (time * 3600);
- break;
- case 4:
- // hhmm Format
- if (separators > 1) {
- // Extraneous separators
- parsed->error = PARSER_INVALID_TZ_OFFSET;
-
- return NULL;
- }
-
- parsed->offset = tz_sign * ((time / 100 * 3600) + (time % 100 * 60));
- break;
- default:
- // Wrong format
- parsed->error = PARSER_INVALID_TZ_OFFSET;
-
- return NULL;
- }
- }
- }
-
- // At this point we should be at the end of the string
- // If not, the string is invalid
- if (*c != '\0') {
- parsed->error = PARSER_INVALID_ISO8601;
-
- return NULL;
- }
-
- return parsed;
-}
-
-
-Parsed* _parse_iso8601_duration(char *str, Parsed *parsed) {
- char* c;
- int value = 0;
- int grabbed = 0;
- int in_time = 0;
- int in_fraction = 0;
- int fraction_length = 0;
- int has_fractional = 0;
- int fraction = 0;
- int has_ymd = 0;
- int has_week = 0;
- int has_year = 0;
- int has_month = 0;
- int has_day = 0;
- int has_hour = 0;
- int has_minute = 0;
- int has_second = 0;
-
- c = str;
-
- // Removing P operator
- c++;
-
- parsed->is_duration = 1;
-
- for (; *c != '\0'; c++) {
- switch (*c) {
- case 'Y':
- if (!grabbed || in_time || has_week || has_ymd) {
- // No value grabbed
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- if (fraction) {
- parsed->error = PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED;
-
- return NULL;
- }
-
- parsed->years = value;
-
- grabbed = 0;
- value = 0;
- fraction = 0;
- in_fraction = 0;
- has_ymd = 1;
- has_year = 1;
-
- break;
- case 'M':
- if (!grabbed || has_week) {
- // No value grabbed
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- if (in_time) {
- if (has_second) {
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- if (has_fractional) {
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- parsed->minutes = value;
- if (fraction) {
- parsed->seconds = fraction * 6;
- has_fractional = 1;
- }
-
- has_minute = 1;
- } else {
- if (fraction) {
- parsed->error = PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED;
-
- return NULL;
- }
-
- if (has_month || has_day) {
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- parsed->months = value;
- has_ymd = 1;
- has_month = 1;
- }
-
- grabbed = 0;
- value = 0;
- fraction = 0;
- in_fraction = 0;
-
- break;
- case 'D':
- if (!grabbed || in_time || has_week) {
- // No value grabbed
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- if (has_day) {
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- parsed->days = value;
- if (fraction) {
- parsed->hours = fraction * 2.4;
- has_fractional = 1;
- }
-
- grabbed = 0;
- value = 0;
- fraction = 0;
- in_fraction = 0;
- has_ymd = 1;
- has_day = 1;
-
- break;
- case 'T':
- if (grabbed) {
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- in_time = 1;
-
- break;
- case 'H':
- if (!grabbed || !in_time || has_week) {
- // No value grabbed
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- if (has_hour || has_second || has_minute) {
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- if (has_fractional) {
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- parsed->hours = value;
- if (fraction) {
- parsed->minutes = fraction * 6;
- has_fractional = 1;
- }
-
- grabbed = 0;
- value = 0;
- fraction = 0;
- in_fraction = 0;
- has_hour = 1;
-
- break;
- case 'S':
- if (!grabbed || !in_time || has_week) {
- // No value grabbed
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- if (has_second) {
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- if (has_fractional) {
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- if (fraction) {
- parsed->seconds = value;
- if (fraction_length > 6) {
- parsed->microseconds = fraction / pow(10, fraction_length - 6);
- } else {
- parsed->microseconds = fraction * pow(10, 6 - fraction_length);
- }
- has_fractional = 1;
- } else {
- parsed->seconds = value;
- }
-
- grabbed = 0;
- value = 0;
- fraction = 0;
- in_fraction = 0;
- has_second = 1;
-
- break;
- case 'W':
- if (!grabbed || in_time || has_ymd) {
- // No value grabbed
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- parsed->weeks = value;
- if (fraction) {
- float days;
- days = fraction * 0.7;
- parsed->hours = (int) ((days - (int) days) * 24);
- parsed->days = (int) days;
- }
-
- grabbed = 0;
- value = 0;
- fraction = 0;
- in_fraction = 0;
- has_week = 1;
-
- break;
- case '.':
- if (!grabbed || has_fractional) {
- // No value grabbed
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- in_fraction = 1;
-
- break;
- case ',':
- if (!grabbed || has_fractional) {
- // No value grabbed
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
-
- in_fraction = 1;
-
- break;
- default:
- if (*c >= '0' && *c <='9') {
- if (in_fraction) {
- fraction = 10 * fraction + *c - '0';
- fraction_length++;
- } else {
- value = 10 * value + *c - '0';
- grabbed = 1;
- }
- break;
- }
-
- parsed->error = PARSER_INVALID_DURATION;
-
- return NULL;
- }
- }
-
- return parsed;
-}
-
-
-PyObject* parse_iso8601(PyObject *self, PyObject *args) {
- char* str;
- PyObject *obj;
- PyObject *tzinfo;
- Parsed *parsed = new_parsed();
-
- if (!PyArg_ParseTuple(args, "s", &str)) {
- PyErr_SetString(
- PyExc_ValueError, "Invalid parameters"
- );
- return NULL;
- }
-
- if (*str == 'P') {
- // Duration (or interval)
- if (_parse_iso8601_duration(str, parsed) == NULL) {
- PyErr_SetString(
- PyExc_ValueError, PARSER_ERRORS[parsed->error]
- );
-
- return NULL;
- }
- } else if (_parse_iso8601_datetime(str, parsed) == NULL) {
- PyErr_SetString(
- PyExc_ValueError, PARSER_ERRORS[parsed->error]
- );
-
- return NULL;
- }
-
- if (parsed->is_date) {
- // Date only
- if (parsed->ambiguous) {
- // We can "safely" assume that the ambiguous
- // date was actually a time in the form hhmmss
- parsed->hour = parsed->year / 100;
- parsed->minute = parsed->year % 100;
- parsed->second = parsed->month;
-
- obj = PyDateTimeAPI->Time_FromTime(
- parsed->hour, parsed->minute, parsed->second, parsed->microsecond,
- Py_BuildValue(""),
- PyDateTimeAPI->TimeType
- );
- } else {
- obj = PyDateTimeAPI->Date_FromDate(
- parsed->year, parsed->month, parsed->day,
- PyDateTimeAPI->DateType
- );
- }
- } else if (parsed->is_datetime) {
- if (!parsed->has_offset) {
- tzinfo = Py_BuildValue("");
- } else {
- tzinfo = new_fixed_offset(parsed->offset, parsed->tzname);
- }
-
- obj = PyDateTimeAPI->DateTime_FromDateAndTime(
- parsed->year,
- parsed->month,
- parsed->day,
- parsed->hour,
- parsed->minute,
- parsed->second,
- parsed->microsecond,
- tzinfo,
- PyDateTimeAPI->DateTimeType
- );
-
- Py_DECREF(tzinfo);
- } else if (parsed->is_duration) {
- obj = new_duration(
- parsed->years, parsed->months, parsed->weeks, parsed->days,
- parsed->hours, parsed->minutes, parsed->seconds, parsed->microseconds
- );
- } else {
- return NULL;
- }
-
- free(parsed);
-
- return obj;
-}
-
-
-/* ------------------------------------------------------------------------- */
-
-static PyMethodDef helpers_methods[] = {
- {
- "parse_iso8601",
- (PyCFunction) parse_iso8601,
- METH_VARARGS,
- PyDoc_STR("Parses a ISO8601 string into a tuple.")
- },
- {NULL}
-};
-
-
-/* ------------------------------------------------------------------------- */
-
-static struct PyModuleDef moduledef = {
- PyModuleDef_HEAD_INIT,
- "_iso8601",
- NULL,
- -1,
- helpers_methods,
- NULL,
- NULL,
- NULL,
- NULL,
-};
-
-PyMODINIT_FUNC
-PyInit__iso8601(void)
-{
- PyObject *module;
-
- PyDateTime_IMPORT;
-
- module = PyModule_Create(&moduledef);
-
- if (module == NULL)
- return NULL;
-
- // FixedOffset declaration
- FixedOffset_type.tp_new = PyType_GenericNew;
- FixedOffset_type.tp_base = PyDateTimeAPI->TZInfoType;
- FixedOffset_type.tp_methods = FixedOffset_methods;
- FixedOffset_type.tp_members = FixedOffset_members;
- FixedOffset_type.tp_init = (initproc)FixedOffset_init;
-
- if (PyType_Ready(&FixedOffset_type) < 0)
- return NULL;
-
- // Duration declaration
- Duration_type.tp_new = PyType_GenericNew;
- Duration_type.tp_members = Duration_members;
- Duration_type.tp_init = (initproc)Duration_init;
-
- if (PyType_Ready(&Duration_type) < 0)
- return NULL;
-
- Py_INCREF(&FixedOffset_type);
- Py_INCREF(&Duration_type);
-
- PyModule_AddObject(module, "TZFixedOffset", (PyObject *)&FixedOffset_type);
- PyModule_AddObject(module, "Duration", (PyObject *)&Duration_type);
-
- return module;
-}
+/* ------------------------------------------------------------------------- */
+
+#include <Python.h>
+#include <datetime.h>
+#include <structmember.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+#ifndef PyVarObject_HEAD_INIT
+#define PyVarObject_HEAD_INIT(type, size) PyObject_HEAD_INIT(type) size,
+#endif
+
+
+/* ------------------------------------------------------------------------- */
+
+#define EPOCH_YEAR 1970
+
+#define DAYS_PER_N_YEAR 365
+#define DAYS_PER_L_YEAR 366
+
+#define USECS_PER_SEC 1000000
+
+#define SECS_PER_MIN 60
+#define SECS_PER_HOUR (60 * SECS_PER_MIN)
+#define SECS_PER_DAY (SECS_PER_HOUR * 24)
+
+// 400-year chunks always have 146097 days (20871 weeks).
+#define DAYS_PER_400_YEARS 146097L
+#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)SECS_PER_DAY)
+
+// The number of seconds in an aligned 100-year chunk, for those that
+// do not begin with a leap year and those that do respectively.
+const int64_t SECS_PER_100_YEARS[2] = {
+ (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY,
+ (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * DAYS_PER_L_YEAR) * SECS_PER_DAY
+};
+
+// The number of seconds in an aligned 4-year chunk, for those that
+// do not begin with a leap year and those that do respectively.
+const int32_t SECS_PER_4_YEARS[2] = {
+ (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY,
+ (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY
+};
+
+// The number of seconds in non-leap and leap years respectively.
+const int32_t SECS_PER_YEAR[2] = {
+ DAYS_PER_N_YEAR * SECS_PER_DAY,
+ DAYS_PER_L_YEAR * SECS_PER_DAY
+};
+
+#define MONTHS_PER_YEAR 12
+
+// The month lengths in non-leap and leap years respectively.
+const int32_t DAYS_PER_MONTHS[2][13] = {
+ {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
+ {-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
+};
+
+// The day offsets of the beginning of each (1-based) month in non-leap
+// and leap years respectively.
+// For example, in a leap year there are 335 days before December.
+const int32_t MONTHS_OFFSETS[2][14] = {
+ {-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365},
+ {-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}
+};
+
+const int DAY_OF_WEEK_TABLE[12] = {
+ 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4
+};
+
+#define TM_SUNDAY 0
+#define TM_MONDAY 1
+#define TM_TUESDAY 2
+#define TM_WEDNESDAY 3
+#define TM_THURSDAY 4
+#define TM_FRIDAY 5
+#define TM_SATURDAY 6
+
+#define TM_JANUARY 0
+#define TM_FEBRUARY 1
+#define TM_MARCH 2
+#define TM_APRIL 3
+#define TM_MAY 4
+#define TM_JUNE 5
+#define TM_JULY 6
+#define TM_AUGUST 7
+#define TM_SEPTEMBER 8
+#define TM_OCTOBER 9
+#define TM_NOVEMBER 10
+#define TM_DECEMBER 11
+
+// Parsing errors
+const int PARSER_INVALID_ISO8601 = 0;
+const int PARSER_INVALID_DATE = 1;
+const int PARSER_INVALID_TIME = 2;
+const int PARSER_INVALID_WEEK_DATE = 3;
+const int PARSER_INVALID_WEEK_NUMBER = 4;
+const int PARSER_INVALID_WEEKDAY_NUMBER = 5;
+const int PARSER_INVALID_ORDINAL_DAY_FOR_YEAR = 6;
+const int PARSER_INVALID_MONTH_OR_DAY = 7;
+const int PARSER_INVALID_MONTH = 8;
+const int PARSER_INVALID_DAY_FOR_MONTH = 9;
+const int PARSER_INVALID_HOUR = 10;
+const int PARSER_INVALID_MINUTE = 11;
+const int PARSER_INVALID_SECOND = 12;
+const int PARSER_INVALID_SUBSECOND = 13;
+const int PARSER_INVALID_TZ_OFFSET = 14;
+const int PARSER_INVALID_DURATION = 15;
+const int PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED = 16;
+
+const char PARSER_ERRORS[17][80] = {
+ "Invalid ISO 8601 string",
+ "Invalid date",
+ "Invalid time",
+ "Invalid week date",
+ "Invalid week number",
+ "Invalid weekday number",
+ "Invalid ordinal day for year",
+ "Invalid month and/or day",
+ "Invalid month",
+ "Invalid day for month",
+ "Invalid hour",
+ "Invalid minute",
+ "Invalid second",
+ "Invalid subsecond",
+ "Invalid timezone offset",
+ "Invalid duration",
+ "Float years and months are not supported"
+};
+
+/* ------------------------------------------------------------------------- */
+
+
+int p(int y) {
+ return y + y/4 - y/100 + y/400;
+}
+
+int is_leap(int year) {
+ return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
+}
+
+int week_day(int year, int month, int day) {
+ int y;
+ int w;
+
+ y = year - (month < 3);
+
+ w = (p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7;
+
+ if (!w) {
+ w = 7;
+ }
+
+ return w;
+}
+
+int days_in_year(int year) {
+ if (is_leap(year)) {
+ return DAYS_PER_L_YEAR;
+ }
+
+ return DAYS_PER_N_YEAR;
+}
+
+int is_long_year(int year) {
+ return (p(year) % 7 == 4) || (p(year - 1) % 7 == 3);
+}
+
+
+/* ------------------------ Custom Types ------------------------------- */
+
+
+/*
+ * class FixedOffset(tzinfo):
+ */
+typedef struct {
+ PyObject_HEAD
+ int offset;
+ char *tzname;
+} FixedOffset;
+
+/*
+ * def __init__(self, offset):
+ * self.offset = offset
+*/
+static int FixedOffset_init(FixedOffset *self, PyObject *args, PyObject *kwargs) {
+ int offset;
+ char *tzname = NULL;
+
+ static char *kwlist[] = {"offset", "tzname", NULL};
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|s", kwlist, &offset, &tzname))
+ return -1;
+
+ self->offset = offset;
+ self->tzname = tzname;
+
+ return 0;
+}
+
+/*
+ * def utcoffset(self, dt):
+ * return timedelta(seconds=self.offset * 60)
+ */
+static PyObject *FixedOffset_utcoffset(FixedOffset *self, PyObject *args) {
+ return PyDelta_FromDSU(0, self->offset, 0);
+}
+
+/*
+ * def dst(self, dt):
+ * return timedelta(seconds=self.offset * 60)
+ */
+static PyObject *FixedOffset_dst(FixedOffset *self, PyObject *args) {
+ return PyDelta_FromDSU(0, self->offset, 0);
+}
+
+/*
+ * def tzname(self, dt):
+ * sign = '+'
+ * if self.offset < 0:
+ * sign = '-'
+ * return f"{sign}{self.offset / 60}:{self.offset % 60}"
+ */
+static PyObject *FixedOffset_tzname(FixedOffset *self, PyObject *args) {
+ if (self->tzname != NULL) {
+ return PyUnicode_FromString(self->tzname);
+ }
+
+ char sign = '+';
+ int offset = self->offset;
+
+ if (offset < 0) {
+ sign = '-';
+ offset *= -1;
+ }
+
+ return PyUnicode_FromFormat(
+ "%c%02d:%02d",
+ sign,
+ offset / SECS_PER_HOUR,
+ offset / SECS_PER_MIN % SECS_PER_MIN
+ );
+}
+
+/*
+ * def __repr__(self):
+ * return self.tzname()
+ */
+static PyObject *FixedOffset_repr(FixedOffset *self) {
+ return FixedOffset_tzname(self, NULL);
+}
+
+/*
+ * Class member / class attributes
+ */
+static PyMemberDef FixedOffset_members[] = {
+ {"offset", T_INT, offsetof(FixedOffset, offset), 0, "UTC offset"},
+ {NULL}
+};
+
+/*
+ * Class methods
+ */
+static PyMethodDef FixedOffset_methods[] = {
+ {"utcoffset", (PyCFunction)FixedOffset_utcoffset, METH_VARARGS, ""},
+ {"dst", (PyCFunction)FixedOffset_dst, METH_VARARGS, ""},
+ {"tzname", (PyCFunction)FixedOffset_tzname, METH_VARARGS, ""},
+ {NULL}
+};
+
+static PyTypeObject FixedOffset_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "FixedOffset_type", /* tp_name */
+ sizeof(FixedOffset), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ 0, /* tp_dealloc */
+ 0, /* tp_print */
+ 0, /* tp_getattr */
+ 0, /* tp_setattr */
+ 0, /* tp_as_async */
+ (reprfunc)FixedOffset_repr, /* tp_repr */
+ 0, /* tp_as_number */
+ 0, /* tp_as_sequence */
+ 0, /* tp_as_mapping */
+ 0, /* tp_hash */
+ 0, /* tp_call */
+ (reprfunc)FixedOffset_repr, /* tp_str */
+ 0, /* tp_getattro */
+ 0, /* tp_setattro */
+ 0, /* tp_as_buffer */
+ Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */
+ "TZInfo with fixed offset", /* tp_doc */
+};
+
+/*
+ * Instantiate new FixedOffset_type object
+ * Skip overhead of calling PyObject_New and PyObject_Init.
+ * Directly allocate object.
+ */
+static PyObject *new_fixed_offset_ex(int offset, char *name, PyTypeObject *type) {
+ FixedOffset *self = (FixedOffset *) (type->tp_alloc(type, 0));
+
+ if (self != NULL) {
+ self->offset = offset;
+ self->tzname = name;
+ }
+
+ return (PyObject *) self;
+}
+
+#define new_fixed_offset(offset, name) new_fixed_offset_ex(offset, name, &FixedOffset_type)
+
+
+/*
+ * class Duration():
+ */
+typedef struct {
+ PyObject_HEAD
+ int years;
+ int months;
+ int weeks;
+ int days;
+ int hours;
+ int minutes;
+ int seconds;
+ int microseconds;
+} Duration;
+
+/*
+ * def __init__(self, years, months, days, hours, minutes, seconds, microseconds):
+ * self.years = years
+ * # ...
+*/
+static int Duration_init(Duration *self, PyObject *args, PyObject *kwargs) {
+ int years;
+ int months;
+ int weeks;
+ int days;
+ int hours;
+ int minutes;
+ int seconds;
+ int microseconds;
+
+ if (!PyArg_ParseTuple(args, "iiiiiiii", &years, &months, &weeks, &days, &hours, &minutes, &seconds, &microseconds))
+ return -1;
+
+ self->years = years;
+ self->months = months;
+ self->weeks = weeks;
+ self->days = days;
+ self->hours = hours;
+ self->minutes = minutes;
+ self->seconds = seconds;
+ self->microseconds = microseconds;
+
+ return 0;
+}
+
+/*
+ * def __repr__(self):
+ * return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format(
+ * self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds
+ * )
+ */
+static PyObject *Duration_repr(Duration *self) {
+ return PyUnicode_FromFormat(
+ "%d years %d months %d weeks %d days %d hours %d minutes %d seconds %d microseconds",
+ self->years,
+ self->months,
+ self->weeks,
+ self->days,
+ self->hours,
+ self->minutes,
+ self->seconds,
+ self->microseconds
+ );
+}
+
+/*
+ * Instantiate new Duration_type object
+ * Skip overhead of calling PyObject_New and PyObject_Init.
+ * Directly allocate object.
+ */
+static PyObject *new_duration_ex(int years, int months, int weeks, int days, int hours, int minutes, int seconds, int microseconds, PyTypeObject *type) {
+ Duration *self = (Duration *) (type->tp_alloc(type, 0));
+
+ if (self != NULL) {
+ self->years = years;
+ self->months = months;
+ self->weeks = weeks;
+ self->days = days;
+ self->hours = hours;
+ self->minutes = minutes;
+ self->seconds = seconds;
+ self->microseconds = microseconds;
+ }
+
+ return (PyObject *) self;
+}
+
+/*
+ * Class member / class attributes
+ */
+static PyMemberDef Duration_members[] = {
+ {"years", T_INT, offsetof(Duration, years), 0, "years in duration"},
+ {"months", T_INT, offsetof(Duration, months), 0, "months in duration"},
+ {"weeks", T_INT, offsetof(Duration, weeks), 0, "weeks in duration"},
+ {"days", T_INT, offsetof(Duration, days), 0, "days in duration"},
+ {"remaining_days", T_INT, offsetof(Duration, days), 0, "days in duration"},
+ {"hours", T_INT, offsetof(Duration, hours), 0, "hours in duration"},
+ {"minutes", T_INT, offsetof(Duration, minutes), 0, "minutes in duration"},
+ {"seconds", T_INT, offsetof(Duration, seconds), 0, "seconds in duration"},
+ {"remaining_seconds", T_INT, offsetof(Duration, seconds), 0, "seconds in duration"},
+ {"microseconds", T_INT, offsetof(Duration, microseconds), 0, "microseconds in duration"},
+ {NULL}
+};
+
+static PyTypeObject Duration_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "Duration", /* tp_name */
+ sizeof(Duration), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ 0, /* tp_dealloc */
+ 0, /* tp_print */
+ 0, /* tp_getattr */
+ 0, /* tp_setattr */
+ 0, /* tp_as_async */
+ (reprfunc)Duration_repr, /* tp_repr */
+ 0, /* tp_as_number */
+ 0, /* tp_as_sequence */
+ 0, /* tp_as_mapping */
+ 0, /* tp_hash */
+ 0, /* tp_call */
+ (reprfunc)Duration_repr, /* tp_str */
+ 0, /* tp_getattro */
+ 0, /* tp_setattro */
+ 0, /* tp_as_buffer */
+ Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */
+ "Duration", /* tp_doc */
+};
+
+#define new_duration(years, months, weeks, days, hours, minutes, seconds, microseconds) new_duration_ex(years, months, weeks, days, hours, minutes, seconds, microseconds, &Duration_type)
+
+typedef struct {
+ int is_date;
+ int is_time;
+ int is_datetime;
+ int is_duration;
+ int is_period;
+ int ambiguous;
+ int year;
+ int month;
+ int day;
+ int hour;
+ int minute;
+ int second;
+ int microsecond;
+ int offset;
+ int has_offset;
+ char *tzname;
+ int years;
+ int months;
+ int weeks;
+ int days;
+ int hours;
+ int minutes;
+ int seconds;
+ int microseconds;
+ int error;
+} Parsed;
+
+
+Parsed* new_parsed() {
+ Parsed *parsed;
+
+ if((parsed = malloc(sizeof *parsed)) != NULL) {
+ parsed->is_date = 0;
+ parsed->is_time = 0;
+ parsed->is_datetime = 0;
+ parsed->is_duration = 0;
+ parsed->is_period = 0;
+
+ parsed->ambiguous = 0;
+ parsed->year = 0;
+ parsed->month = 1;
+ parsed->day = 1;
+ parsed->hour = 0;
+ parsed->minute = 0;
+ parsed->second = 0;
+ parsed->microsecond = 0;
+ parsed->offset = 0;
+ parsed->has_offset = 0;
+ parsed->tzname = NULL;
+
+ parsed->years = 0;
+ parsed->months = 0;
+ parsed->weeks = 0;
+ parsed->days = 0;
+ parsed->hours = 0;
+ parsed->minutes = 0;
+ parsed->seconds = 0;
+ parsed->microseconds = 0;
+
+ parsed->error = -1;
+ }
+
+ return parsed;
+}
+
+
+/* -------------------------- Functions --------------------------*/
+
+Parsed* _parse_iso8601_datetime(char *str, Parsed *parsed) {
+ char* c;
+ int monthday = 0;
+ int week = 0;
+ int weekday = 1;
+ int ordinal;
+ int tz_sign = 0;
+ int leap = 0;
+ int separators = 0;
+ int time = 0;
+ int i;
+ int j;
+
+ // Assuming date only for now
+ parsed->is_date = 1;
+
+ c = str;
+
+ for (i = 0; i < 4; i++) {
+ if (*c >= '0' && *c <= '9') {
+ parsed->year = 10 * parsed->year + *c++ - '0';
+ } else {
+ parsed->error = PARSER_INVALID_ISO8601;
+
+ return NULL;
+ }
+ }
+
+ leap = is_leap(parsed->year);
+
+ // Optional separator
+ if (*c == '-') {
+ separators++;
+ c++;
+ }
+
+ // Checking for week dates
+ if (*c == 'W') {
+ c++;
+
+ i = 0;
+ while (*c != '\0' && *c != ' ' && *c != 'T') {
+ if (*c == '-') {
+ separators++;
+ c++;
+ continue;
+ }
+
+ week = 10 * week + *c++ - '0';
+
+ i++;
+ }
+
+ switch (i) {
+ case 2:
+ // Only week number
+ break;
+ case 3:
+ // Week with weekday
+ if (!(separators == 0 || separators == 2)) {
+ // We should have 2 or no separator
+ parsed->error = PARSER_INVALID_WEEK_DATE;
+
+ return NULL;
+ }
+
+ weekday = week % 10;
+ week /= 10;
+
+ break;
+ default:
+ // Any other case is wrong
+ parsed->error = PARSER_INVALID_WEEK_DATE;
+
+ return NULL;
+ }
+
+ // Checks
+ if (week > 53 || (week > 52 && !is_long_year(parsed->year))) {
+ parsed->error = PARSER_INVALID_WEEK_NUMBER;
+
+ return NULL;
+ }
+
+ if (weekday > 7) {
+ parsed->error = PARSER_INVALID_WEEKDAY_NUMBER;
+
+ return NULL;
+ }
+
+ // Calculating ordinal day
+ ordinal = week * 7 + weekday - (week_day(parsed->year, 1, 4) + 3);
+
+ if (ordinal < 1) {
+ // Previous year
+ ordinal += days_in_year(parsed->year - 1);
+ parsed->year -= 1;
+ leap = is_leap(parsed->year);
+ }
+
+ if (ordinal > days_in_year(parsed->year)) {
+ // Next year
+ ordinal -= days_in_year(parsed->year);
+ parsed->year += 1;
+ leap = is_leap(parsed->year);
+ }
+
+ for (j = 1; j < 14; j++) {
+ if (ordinal <= MONTHS_OFFSETS[leap][j]) {
+ parsed->day = ordinal - MONTHS_OFFSETS[leap][j - 1];
+ parsed->month = j - 1;
+
+ break;
+ }
+ }
+ } else {
+ // At this point we need to check the number
+ // of characters until the end of the date part
+ // (or the end of the string).
+ //
+ // If two, we have only a month if there is a separator, it may be a time otherwise.
+ // If three, we have an ordinal date.
+ // If four, we have a complete date
+ i = 0;
+ while (*c != '\0' && *c != ' ' && *c != 'T') {
+ if (*c == '-') {
+ separators++;
+ c++;
+ continue;
+ }
+
+ if (!(*c >= '0' && *c <='9')) {
+ parsed->error = PARSER_INVALID_DATE;
+
+ return NULL;
+ }
+
+ monthday = 10 * monthday + *c++ - '0';
+
+ i++;
+ }
+
+ switch (i) {
+ case 0:
+ // No month/day specified (only a year)
+ break;
+ case 2:
+ if (!separators) {
+ // The date looks like 201207
+ // which is invalid for a date
+ // But it might be a time in the form hhmmss
+ parsed->ambiguous = 1;
+ } else if (separators > 1) {
+ parsed->error = PARSER_INVALID_DATE;
+
+ return NULL;
+ }
+
+ parsed->month = monthday;
+ break;
+ case 3:
+ // Ordinal day
+ if (separators > 1) {
+ parsed->error = PARSER_INVALID_DATE;
+
+ return NULL;
+ }
+
+ if (monthday < 1 || monthday > MONTHS_OFFSETS[leap][13]) {
+ parsed->error = PARSER_INVALID_ORDINAL_DAY_FOR_YEAR;
+
+ return NULL;
+ }
+
+ for (j = 1; j < 14; j++) {
+ if (monthday <= MONTHS_OFFSETS[leap][j]) {
+ parsed->day = monthday - MONTHS_OFFSETS[leap][j - 1];
+ parsed->month = j - 1;
+
+ break;
+ }
+ }
+
+ break;
+ case 4:
+ // Month and day
+ parsed->month = monthday / 100;
+ parsed->day = monthday % 100;
+
+ break;
+ default:
+ parsed->error = PARSER_INVALID_MONTH_OR_DAY;
+
+ return NULL;
+ }
+ }
+
+ // Checks
+ if (separators && !monthday && !week) {
+ parsed->error = PARSER_INVALID_DATE;
+
+ return NULL;
+ }
+
+ if (parsed->month > 12) {
+ parsed->error = PARSER_INVALID_MONTH;
+
+ return NULL;
+ }
+
+ if (parsed->day > DAYS_PER_MONTHS[leap][parsed->month]) {
+ parsed->error = PARSER_INVALID_DAY_FOR_MONTH;
+
+ return NULL;
+ }
+
+ separators = 0;
+ if (*c == 'T' || *c == ' ') {
+ if (parsed->ambiguous) {
+ parsed->error = PARSER_INVALID_DATE;
+
+ return NULL;
+ }
+
+ // We have time so we have a datetime
+ parsed->is_datetime = 1;
+ parsed->is_date = 0;
+
+ c++;
+
+ // Grabbing time information
+ i = 0;
+ while (*c != '\0' && *c != '.' && *c != ',' && *c != 'Z' && *c != '+' && *c != '-') {
+ if (*c == ':') {
+ separators++;
+ c++;
+ continue;
+ }
+
+ if (!(*c >= '0' && *c <='9')) {
+ parsed->error = PARSER_INVALID_TIME;
+
+ return NULL;
+ }
+
+ time = 10 * time + *c++ - '0';
+ i++;
+ }
+
+ switch (i) {
+ case 2:
+ // Hours only
+ if (separators > 0) {
+ // Extraneous separators
+ parsed->error = PARSER_INVALID_TIME;
+
+ return NULL;
+ }
+
+ parsed->hour = time;
+ break;
+ case 4:
+ // Hours and minutes
+ if (separators > 1) {
+ // Extraneous separators
+ parsed->error = PARSER_INVALID_TIME;
+
+ return NULL;
+ }
+
+ parsed->hour = time / 100;
+ parsed->minute = time % 100;
+ break;
+ case 6:
+ // Hours, minutes and seconds
+ if (!(separators == 0 || separators == 2)) {
+ // We should have either two separators or none
+ parsed->error = PARSER_INVALID_TIME;
+
+ return NULL;
+ }
+
+ parsed->hour = time / 10000;
+ parsed->minute = time / 100 % 100;
+ parsed->second = time % 100;
+ break;
+ default:
+ // Any other case is wrong
+ parsed->error = PARSER_INVALID_TIME;
+
+ return NULL;
+ }
+
+ // Checks
+ if (parsed->hour > 23) {
+ parsed->error = PARSER_INVALID_HOUR;
+
+ return NULL;
+ }
+
+ if (parsed->minute > 59) {
+ parsed->error = PARSER_INVALID_MINUTE;
+
+ return NULL;
+ }
+
+ if (parsed->second > 59) {
+ parsed->error = PARSER_INVALID_SECOND;
+
+ return NULL;
+ }
+
+ // Subsecond
+ if (*c == '.' || *c == ',') {
+ c++;
+
+ time = 0;
+ i = 0;
+ while (*c != '\0' && *c != 'Z' && *c != '+' && *c != '-') {
+ if (!(*c >= '0' && *c <='9')) {
+ parsed->error = PARSER_INVALID_SUBSECOND;
+
+ return NULL;
+ }
+
+ time = 10 * time + *c++ - '0';
+ i++;
+ }
+
+ // adjust to microseconds
+ if (i > 6) {
+ parsed->microsecond = time / pow(10, i - 6);
+ } else if (i <= 6) {
+ parsed->microsecond = time * pow(10, 6 - i);
+ }
+ }
+
+ // Timezone
+ if (*c == 'Z') {
+ parsed->has_offset = 1;
+ parsed->tzname = "UTC";
+ c++;
+ } else if (*c == '+' || *c == '-') {
+ tz_sign = 1;
+ if (*c == '-') {
+ tz_sign = -1;
+ }
+
+ parsed->has_offset = 1;
+ c++;
+
+ i = 0;
+ time = 0;
+ separators = 0;
+ while (*c != '\0') {
+ if (*c == ':') {
+ separators++;
+ c++;
+ continue;
+ }
+
+ if (!(*c >= '0' && *c <= '9')) {
+ parsed->error = PARSER_INVALID_TZ_OFFSET;
+
+ return NULL;
+ }
+
+ time = 10 * time + *c++ - '0';
+ i++;
+ }
+
+ switch (i) {
+ case 2:
+ // hh Format
+ if (separators) {
+ // Extraneous separators
+ parsed->error = PARSER_INVALID_TZ_OFFSET;
+
+ return NULL;
+ }
+
+ parsed->offset = tz_sign * (time * 3600);
+ break;
+ case 4:
+ // hhmm Format
+ if (separators > 1) {
+ // Extraneous separators
+ parsed->error = PARSER_INVALID_TZ_OFFSET;
+
+ return NULL;
+ }
+
+ parsed->offset = tz_sign * ((time / 100 * 3600) + (time % 100 * 60));
+ break;
+ default:
+ // Wrong format
+ parsed->error = PARSER_INVALID_TZ_OFFSET;
+
+ return NULL;
+ }
+ }
+ }
+
+ // At this point we should be at the end of the string
+ // If not, the string is invalid
+ if (*c != '\0') {
+ parsed->error = PARSER_INVALID_ISO8601;
+
+ return NULL;
+ }
+
+ return parsed;
+}
+
+
+Parsed* _parse_iso8601_duration(char *str, Parsed *parsed) {
+ char* c;
+ int value = 0;
+ int grabbed = 0;
+ int in_time = 0;
+ int in_fraction = 0;
+ int fraction_length = 0;
+ int has_fractional = 0;
+ int fraction = 0;
+ int has_ymd = 0;
+ int has_week = 0;
+ int has_month = 0;
+ int has_day = 0;
+ int has_hour = 0;
+ int has_minute = 0;
+ int has_second = 0;
+
+ c = str;
+
+ // Removing P operator
+ c++;
+
+ parsed->is_duration = 1;
+
+ for (; *c != '\0'; c++) {
+ switch (*c) {
+ case 'Y':
+ if (!grabbed || in_time || has_week || has_ymd) {
+ // No value grabbed
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (fraction) {
+ parsed->error = PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED;
+
+ return NULL;
+ }
+
+ parsed->years = value;
+
+ grabbed = 0;
+ value = 0;
+ fraction = 0;
+ in_fraction = 0;
+ has_ymd = 1;
+
+ break;
+ case 'M':
+ if (!grabbed || has_week) {
+ // No value grabbed
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (in_time) {
+ if (has_second) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (has_fractional) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ parsed->minutes = value;
+ if (fraction) {
+ parsed->seconds = fraction * 6;
+ has_fractional = 1;
+ }
+
+ has_minute = 1;
+ } else {
+ if (fraction) {
+ parsed->error = PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED;
+
+ return NULL;
+ }
+
+ if (has_month || has_day) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ parsed->months = value;
+ has_ymd = 1;
+ has_month = 1;
+ }
+
+ grabbed = 0;
+ value = 0;
+ fraction = 0;
+ in_fraction = 0;
+
+ break;
+ case 'D':
+ if (!grabbed || in_time || has_week) {
+ // No value grabbed
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (has_day) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ parsed->days = value;
+ if (fraction) {
+ parsed->hours = fraction * 2.4;
+ has_fractional = 1;
+ }
+
+ grabbed = 0;
+ value = 0;
+ fraction = 0;
+ in_fraction = 0;
+ has_ymd = 1;
+ has_day = 1;
+
+ break;
+ case 'T':
+ if (grabbed) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ in_time = 1;
+
+ break;
+ case 'H':
+ if (!grabbed || !in_time || has_week) {
+ // No value grabbed
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (has_hour || has_second || has_minute) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (has_fractional) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ parsed->hours = value;
+ if (fraction) {
+ parsed->minutes = fraction * 6;
+ has_fractional = 1;
+ }
+
+ grabbed = 0;
+ value = 0;
+ fraction = 0;
+ in_fraction = 0;
+ has_hour = 1;
+
+ break;
+ case 'S':
+ if (!grabbed || !in_time || has_week) {
+ // No value grabbed
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (has_second) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (has_fractional) {
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ if (fraction) {
+ parsed->seconds = value;
+ if (fraction_length > 6) {
+ parsed->microseconds = fraction / pow(10, fraction_length - 6);
+ } else {
+ parsed->microseconds = fraction * pow(10, 6 - fraction_length);
+ }
+ has_fractional = 1;
+ } else {
+ parsed->seconds = value;
+ }
+
+ grabbed = 0;
+ value = 0;
+ fraction = 0;
+ in_fraction = 0;
+ has_second = 1;
+
+ break;
+ case 'W':
+ if (!grabbed || in_time || has_ymd) {
+ // No value grabbed
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ parsed->weeks = value;
+ if (fraction) {
+ float days;
+ days = fraction * 0.7;
+ parsed->hours = (int) ((days - (int) days) * 24);
+ parsed->days = (int) days;
+ }
+
+ grabbed = 0;
+ value = 0;
+ fraction = 0;
+ in_fraction = 0;
+ has_week = 1;
+
+ break;
+ case '.':
+ if (!grabbed || has_fractional) {
+ // No value grabbed
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ in_fraction = 1;
+
+ break;
+ case ',':
+ if (!grabbed || has_fractional) {
+ // No value grabbed
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+
+ in_fraction = 1;
+
+ break;
+ default:
+ if (*c >= '0' && *c <='9') {
+ if (in_fraction) {
+ fraction = 10 * fraction + *c - '0';
+ fraction_length++;
+ } else {
+ value = 10 * value + *c - '0';
+ grabbed = 1;
+ }
+ break;
+ }
+
+ parsed->error = PARSER_INVALID_DURATION;
+
+ return NULL;
+ }
+ }
+
+ return parsed;
+}
+
+
+PyObject* parse_iso8601(PyObject *self, PyObject *args) {
+ char* str;
+ PyObject *obj;
+ PyObject *tzinfo;
+ Parsed *parsed = new_parsed();
+
+ if (!PyArg_ParseTuple(args, "s", &str)) {
+ PyErr_SetString(
+ PyExc_ValueError, "Invalid parameters"
+ );
+ free(parsed);
+ return NULL;
+ }
+
+ if (*str == 'P') {
+ // Duration (or interval)
+ if (_parse_iso8601_duration(str, parsed) == NULL) {
+ PyErr_SetString(
+ PyExc_ValueError, PARSER_ERRORS[parsed->error]
+ );
+
+ free(parsed);
+ return NULL;
+ }
+ } else if (_parse_iso8601_datetime(str, parsed) == NULL) {
+ PyErr_SetString(
+ PyExc_ValueError, PARSER_ERRORS[parsed->error]
+ );
+
+ free(parsed);
+ return NULL;
+ }
+
+ if (parsed->is_date) {
+ // Date only
+ if (parsed->ambiguous) {
+ // We can "safely" assume that the ambiguous
+ // date was actually a time in the form hhmmss
+ parsed->hour = parsed->year / 100;
+ parsed->minute = parsed->year % 100;
+ parsed->second = parsed->month;
+
+ obj = PyDateTimeAPI->Time_FromTime(
+ parsed->hour, parsed->minute, parsed->second, parsed->microsecond,
+ Py_BuildValue(""),
+ PyDateTimeAPI->TimeType
+ );
+ } else {
+ obj = PyDateTimeAPI->Date_FromDate(
+ parsed->year, parsed->month, parsed->day,
+ PyDateTimeAPI->DateType
+ );
+ }
+ } else if (parsed->is_datetime) {
+ if (!parsed->has_offset) {
+ tzinfo = Py_BuildValue("");
+ } else {
+ tzinfo = new_fixed_offset(parsed->offset, parsed->tzname);
+ }
+
+ obj = PyDateTimeAPI->DateTime_FromDateAndTime(
+ parsed->year,
+ parsed->month,
+ parsed->day,
+ parsed->hour,
+ parsed->minute,
+ parsed->second,
+ parsed->microsecond,
+ tzinfo,
+ PyDateTimeAPI->DateTimeType
+ );
+
+ Py_DECREF(tzinfo);
+ } else if (parsed->is_duration) {
+ obj = new_duration(
+ parsed->years, parsed->months, parsed->weeks, parsed->days,
+ parsed->hours, parsed->minutes, parsed->seconds, parsed->microseconds
+ );
+ } else {
+ free(parsed);
+ return NULL;
+ }
+
+ free(parsed);
+
+ return obj;
+}
+
+
+/* ------------------------------------------------------------------------- */
+
+static PyMethodDef helpers_methods[] = {
+ {
+ "parse_iso8601",
+ (PyCFunction) parse_iso8601,
+ METH_VARARGS,
+ PyDoc_STR("Parses a ISO8601 string into a tuple.")
+ },
+ {NULL}
+};
+
+
+/* ------------------------------------------------------------------------- */
+
+static struct PyModuleDef moduledef = {
+ PyModuleDef_HEAD_INIT,
+ "_iso8601",
+ NULL,
+ -1,
+ helpers_methods,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+};
+
+PyMODINIT_FUNC
+PyInit__iso8601(void)
+{
+ PyObject *module;
+
+ PyDateTime_IMPORT;
+
+ module = PyModule_Create(&moduledef);
+
+ if (module == NULL)
+ return NULL;
+
+ // FixedOffset declaration
+ FixedOffset_type.tp_new = PyType_GenericNew;
+ FixedOffset_type.tp_base = PyDateTimeAPI->TZInfoType;
+ FixedOffset_type.tp_methods = FixedOffset_methods;
+ FixedOffset_type.tp_members = FixedOffset_members;
+ FixedOffset_type.tp_init = (initproc)FixedOffset_init;
+
+ if (PyType_Ready(&FixedOffset_type) < 0)
+ return NULL;
+
+ // Duration declaration
+ Duration_type.tp_new = PyType_GenericNew;
+ Duration_type.tp_members = Duration_members;
+ Duration_type.tp_init = (initproc)Duration_init;
+
+ if (PyType_Ready(&Duration_type) < 0)
+ return NULL;
+
+ Py_INCREF(&FixedOffset_type);
+ Py_INCREF(&Duration_type);
+
+ PyModule_AddObject(module, "TZFixedOffset", (PyObject *)&FixedOffset_type);
+ PyModule_AddObject(module, "Duration", (PyObject *)&Duration_type);
+
+ return module;
+}
diff --git a/pendulum/parsing/_iso8601.pyi b/pendulum/parsing/_iso8601.pyi
new file mode 100644
index 0000000..b9ce5d4
--- /dev/null
+++ b/pendulum/parsing/_iso8601.pyi
@@ -0,0 +1,22 @@
+from __future__ import annotations
+
+from datetime import date
+from datetime import datetime
+from datetime import time
+
+class Duration:
+
+ years: int = 0
+ months: int = 0
+ weeks: int = 0
+ days: int = 0
+ remaining_days: int = 0
+ hours: int = 0
+ minutes: int = 0
+ seconds: int = 0
+ remaining_seconds: int = 0
+ microseconds: int = 0
+
+def parse_iso8601(
+ text: str,
+) -> datetime | date | time | Duration: ...
diff --git a/pendulum/parsing/exceptions/__init__.py b/pendulum/parsing/exceptions/__init__.py
index 997b0fa..05195b5 100644
--- a/pendulum/parsing/exceptions/__init__.py
+++ b/pendulum/parsing/exceptions/__init__.py
@@ -1,3 +1,6 @@
-class ParserError(ValueError):
-
- pass
+from __future__ import annotations
+
+
+class ParserError(ValueError):
+
+ pass
diff --git a/pendulum/parsing/iso8601.py b/pendulum/parsing/iso8601.py
index 40efa2f..907cf13 100644
--- a/pendulum/parsing/iso8601.py
+++ b/pendulum/parsing/iso8601.py
@@ -1,447 +1,454 @@
-from __future__ import division
-
-import datetime
-import re
-
-from ..constants import HOURS_PER_DAY
-from ..constants import MINUTES_PER_HOUR
-from ..constants import MONTHS_OFFSETS
-from ..constants import SECONDS_PER_MINUTE
-from ..duration import Duration
-from ..helpers import days_in_year
-from ..helpers import is_leap
-from ..helpers import is_long_year
-from ..helpers import week_day
-from ..tz.timezone import UTC
-from ..tz.timezone import FixedTimezone
-from .exceptions import ParserError
-
-
-ISO8601_DT = re.compile(
- # Date (optional)
- "^"
- "(?P<date>"
- " (?P<classic>" # Classic date (YYYY-MM-DD) or ordinal (YYYY-DDD)
- r" (?P<year>\d{4})" # Year
- " (?P<monthday>"
- r" (?P<monthsep>-)?(?P<month>\d{2})" # Month (optional)
- r" ((?P<daysep>-)?(?P<day>\d{1,2}))?" # Day (optional)
- " )?"
- " )"
- " |"
- " (?P<isocalendar>" # Calendar date (2016-W05 or 2016-W05-5)
- r" (?P<isoyear>\d{4})" # Year
- " (?P<weeksep>-)?" # Separator (optional)
- " W" # W separator
- r" (?P<isoweek>\d{2})" # Week number
- " (?P<weekdaysep>-)?" # Separator (optional)
- r" (?P<isoweekday>\d)?" # Weekday (optional)
- " )"
- ")?"
- # Time (optional)
- "(?P<time>"
- r" (?P<timesep>[T\ ])?" # Separator (T or space)
- r" (?P<hour>\d{1,2})(?P<minsep>:)?(?P<minute>\d{1,2})?(?P<secsep>:)?(?P<second>\d{1,2})?" # HH:mm:ss (optional mm and ss)
- # Subsecond part (optional)
- " (?P<subsecondsection>"
- " (?:[.,])" # Subsecond separator (optional)
- r" (?P<subsecond>\d{1,9})" # Subsecond
- " )?"
- # Timezone offset
- " (?P<tz>"
- r" (?:[-+])\d{2}:?(?:\d{2})?|Z" # Offset (+HH:mm or +HHmm or +HH or Z)
- " )?"
- ")?"
- "$",
- re.VERBOSE,
-)
-
-
-ISO8601_DURATION = re.compile(
- "^P" # Duration P indicator
- # Years, months and days (optional)
- "(?P<w>"
- r" (?P<weeks>\d+(?:[.,]\d+)?W)"
- ")?"
- "(?P<ymd>"
- r" (?P<years>\d+(?:[.,]\d+)?Y)?"
- r" (?P<months>\d+(?:[.,]\d+)?M)?"
- r" (?P<days>\d+(?:[.,]\d+)?D)?"
- ")?"
- "(?P<hms>"
- " (?P<timesep>T)" # Separator (T)
- r" (?P<hours>\d+(?:[.,]\d+)?H)?"
- r" (?P<minutes>\d+(?:[.,]\d+)?M)?"
- r" (?P<seconds>\d+(?:[.,]\d+)?S)?"
- ")?"
- "$",
- re.VERBOSE,
-)
-
-
-def parse_iso8601(text):
- """
- ISO 8601 compliant parser.
-
- :param text: The string to parse
- :type text: str
-
- :rtype: datetime.datetime or datetime.time or datetime.date
- """
- parsed = _parse_iso8601_duration(text)
- if parsed is not None:
- return parsed
-
- m = ISO8601_DT.match(text)
- if not m:
- raise ParserError("Invalid ISO 8601 string")
-
- ambiguous_date = False
- is_date = False
- is_time = False
- year = 0
- month = 1
- day = 1
- minute = 0
- second = 0
- microsecond = 0
- tzinfo = None
-
- if m:
- if m.group("date"):
- # A date has been specified
- is_date = True
-
- if m.group("isocalendar"):
- # We have a ISO 8601 string defined
- # by week number
- if (
- m.group("weeksep")
- and not m.group("weekdaysep")
- and m.group("isoweekday")
- ):
- raise ParserError("Invalid date string: {}".format(text))
-
- if not m.group("weeksep") and m.group("weekdaysep"):
- raise ParserError("Invalid date string: {}".format(text))
-
- try:
- date = _get_iso_8601_week(
- m.group("isoyear"), m.group("isoweek"), m.group("isoweekday")
- )
- except ParserError:
- raise
- except ValueError:
- raise ParserError("Invalid date string: {}".format(text))
-
- year = date["year"]
- month = date["month"]
- day = date["day"]
- else:
- # We have a classic date representation
- year = int(m.group("year"))
-
- if not m.group("monthday"):
- # No month and day
- month = 1
- day = 1
- else:
- if m.group("month") and m.group("day"):
- # Month and day
- if not m.group("daysep") and len(m.group("day")) == 1:
- # Ordinal day
- ordinal = int(m.group("month") + m.group("day"))
- leap = is_leap(year)
- months_offsets = MONTHS_OFFSETS[leap]
-
- if ordinal > months_offsets[13]:
- raise ParserError("Ordinal day is out of range")
-
- for i in range(1, 14):
- if ordinal <= months_offsets[i]:
- day = ordinal - months_offsets[i - 1]
- month = i - 1
-
- break
- else:
- month = int(m.group("month"))
- day = int(m.group("day"))
- else:
- # Only month
- if not m.group("monthsep"):
- # The date looks like 201207
- # which is invalid for a date
- # But it might be a time in the form hhmmss
- ambiguous_date = True
-
- month = int(m.group("month"))
- day = 1
-
- if not m.group("time"):
- # No time has been specified
- if ambiguous_date:
- # We can "safely" assume that the ambiguous date
- # was actually a time in the form hhmmss
- hhmmss = "{}{:0>2}".format(str(year), str(month))
-
- return datetime.time(int(hhmmss[:2]), int(hhmmss[2:4]), int(hhmmss[4:]))
-
- return datetime.date(year, month, day)
-
- if ambiguous_date:
- raise ParserError("Invalid date string: {}".format(text))
-
- if is_date and not m.group("timesep"):
- raise ParserError("Invalid date string: {}".format(text))
-
- if not is_date:
- is_time = True
-
- # Grabbing hh:mm:ss
- hour = int(m.group("hour"))
- minsep = m.group("minsep")
-
- if m.group("minute"):
- minute = int(m.group("minute"))
- elif minsep:
- raise ParserError("Invalid ISO 8601 time part")
-
- secsep = m.group("secsep")
- if secsep and not minsep and m.group("minute"):
- # minute/second separator but no hour/minute separator
- raise ParserError("Invalid ISO 8601 time part")
-
- if m.group("second"):
- if not secsep and minsep:
- # No minute/second separator but hour/minute separator
- raise ParserError("Invalid ISO 8601 time part")
-
- second = int(m.group("second"))
- elif secsep:
- raise ParserError("Invalid ISO 8601 time part")
-
- # Grabbing subseconds, if any
- if m.group("subsecondsection"):
- # Limiting to 6 chars
- subsecond = m.group("subsecond")[:6]
-
- microsecond = int("{:0<6}".format(subsecond))
-
- # Grabbing timezone, if any
- tz = m.group("tz")
- if tz:
- if tz == "Z":
- tzinfo = UTC
- else:
- negative = True if tz.startswith("-") else False
- tz = tz[1:]
- if ":" not in tz:
- if len(tz) == 2:
- tz = "{}00".format(tz)
-
- off_hour = tz[0:2]
- off_minute = tz[2:4]
- else:
- off_hour, off_minute = tz.split(":")
-
- offset = ((int(off_hour) * 60) + int(off_minute)) * 60
-
- if negative:
- offset = -1 * offset
-
- tzinfo = FixedTimezone(offset)
-
- if is_time:
- return datetime.time(hour, minute, second, microsecond)
-
- return datetime.datetime(
- year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo
- )
-
-
-def _parse_iso8601_duration(text, **options):
- m = ISO8601_DURATION.match(text)
- if not m:
- return
-
- years = 0
- months = 0
- weeks = 0
- days = 0
- hours = 0
- minutes = 0
- seconds = 0
- microseconds = 0
- fractional = False
-
- if m.group("w"):
- # Weeks
- if m.group("ymd") or m.group("hms"):
- # Specifying anything more than weeks is not supported
- raise ParserError("Invalid duration string")
-
- _weeks = m.group("weeks")
- if not _weeks:
- raise ParserError("Invalid duration string")
-
- _weeks = _weeks.replace(",", ".").replace("W", "")
- if "." in _weeks:
- _weeks, portion = _weeks.split(".")
- weeks = int(_weeks)
- _days = int(portion) / 10 * 7
- days, hours = int(_days // 1), _days % 1 * HOURS_PER_DAY
- else:
- weeks = int(_weeks)
-
- if m.group("ymd"):
- # Years, months and/or days
- _years = m.group("years")
- _months = m.group("months")
- _days = m.group("days")
-
- # Checking order
- years_start = m.start("years") if _years else -3
- months_start = m.start("months") if _months else years_start + 1
- days_start = m.start("days") if _days else months_start + 1
-
- # Check correct order
- if not (years_start < months_start < days_start):
- raise ParserError("Invalid duration")
-
- if _years:
- _years = _years.replace(",", ".").replace("Y", "")
- if "." in _years:
- raise ParserError("Float years in duration are not supported")
- else:
- years = int(_years)
-
- if _months:
- if fractional:
- raise ParserError("Invalid duration")
-
- _months = _months.replace(",", ".").replace("M", "")
- if "." in _months:
- raise ParserError("Float months in duration are not supported")
- else:
- months = int(_months)
-
- if _days:
- if fractional:
- raise ParserError("Invalid duration")
-
- _days = _days.replace(",", ".").replace("D", "")
-
- if "." in _days:
- fractional = True
-
- _days, _hours = _days.split(".")
- days = int(_days)
- hours = int(_hours) / 10 * HOURS_PER_DAY
- else:
- days = int(_days)
-
- if m.group("hms"):
- # Hours, minutes and/or seconds
- _hours = m.group("hours") or 0
- _minutes = m.group("minutes") or 0
- _seconds = m.group("seconds") or 0
-
- # Checking order
- hours_start = m.start("hours") if _hours else -3
- minutes_start = m.start("minutes") if _minutes else hours_start + 1
- seconds_start = m.start("seconds") if _seconds else minutes_start + 1
-
- # Check correct order
- if not (hours_start < minutes_start < seconds_start):
- raise ParserError("Invalid duration")
-
- if _hours:
- if fractional:
- raise ParserError("Invalid duration")
-
- _hours = _hours.replace(",", ".").replace("H", "")
-
- if "." in _hours:
- fractional = True
-
- _hours, _mins = _hours.split(".")
- hours += int(_hours)
- minutes += int(_mins) / 10 * MINUTES_PER_HOUR
- else:
- hours += int(_hours)
-
- if _minutes:
- if fractional:
- raise ParserError("Invalid duration")
-
- _minutes = _minutes.replace(",", ".").replace("M", "")
-
- if "." in _minutes:
- fractional = True
-
- _minutes, _secs = _minutes.split(".")
- minutes += int(_minutes)
- seconds += int(_secs) / 10 * SECONDS_PER_MINUTE
- else:
- minutes += int(_minutes)
-
- if _seconds:
- if fractional:
- raise ParserError("Invalid duration")
-
- _seconds = _seconds.replace(",", ".").replace("S", "")
-
- if "." in _seconds:
- _seconds, _microseconds = _seconds.split(".")
- seconds += int(_seconds)
- microseconds += int("{:0<6}".format(_microseconds[:6]))
- else:
- seconds += int(_seconds)
-
- return Duration(
- years=years,
- months=months,
- weeks=weeks,
- days=days,
- hours=hours,
- minutes=minutes,
- seconds=seconds,
- microseconds=microseconds,
- )
-
-
-def _get_iso_8601_week(year, week, weekday):
- if not weekday:
- weekday = 1
- else:
- weekday = int(weekday)
-
- year = int(year)
- week = int(week)
-
- if week > 53 or week > 52 and not is_long_year(year):
- raise ParserError("Invalid week for week date")
-
- if weekday > 7:
- raise ParserError("Invalid weekday for week date")
-
- # We can't rely on strptime directly here since
- # it does not support ISO week date
- ordinal = week * 7 + weekday - (week_day(year, 1, 4) + 3)
-
- if ordinal < 1:
- # Previous year
- ordinal += days_in_year(year - 1)
- year -= 1
-
- if ordinal > days_in_year(year):
- # Next year
- ordinal -= days_in_year(year)
- year += 1
-
- fmt = "%Y-%j"
- string = "{}-{}".format(year, ordinal)
-
- dt = datetime.datetime.strptime(string, fmt)
-
- return {"year": dt.year, "month": dt.month, "day": dt.day}
+from __future__ import annotations
+
+import datetime
+import re
+
+from typing import cast
+
+from pendulum.constants import HOURS_PER_DAY
+from pendulum.constants import MINUTES_PER_HOUR
+from pendulum.constants import MONTHS_OFFSETS
+from pendulum.constants import SECONDS_PER_MINUTE
+from pendulum.duration import Duration
+from pendulum.helpers import days_in_year
+from pendulum.helpers import is_leap
+from pendulum.helpers import is_long_year
+from pendulum.helpers import week_day
+from pendulum.parsing.exceptions import ParserError
+from pendulum.tz.timezone import UTC
+from pendulum.tz.timezone import FixedTimezone
+
+ISO8601_DT = re.compile(
+ # Date (optional) # noqa: E800
+ "^"
+ "(?P<date>"
+ " (?P<classic>" # Classic date (YYYY-MM-DD) or ordinal (YYYY-DDD)
+ r" (?P<year>\d{4})" # Year
+ " (?P<monthday>"
+ r" (?P<monthsep>-)?(?P<month>\d{2})" # Month (optional)
+ r" ((?P<daysep>-)?(?P<day>\d{1,2}))?" # Day (optional)
+ " )?"
+ " )"
+ " |"
+ " (?P<isocalendar>" # Calendar date (2016-W05 or 2016-W05-5)
+ r" (?P<isoyear>\d{4})" # Year
+ " (?P<weeksep>-)?" # Separator (optional)
+ " W" # W separator
+ r" (?P<isoweek>\d{2})" # Week number
+ " (?P<weekdaysep>-)?" # Separator (optional)
+ r" (?P<isoweekday>\d)?" # Weekday (optional)
+ " )"
+ ")?"
+ # Time (optional) # noqa: E800
+ "(?P<time>"
+ r" (?P<timesep>[T\ ])?" # Separator (T or space)
+ r" (?P<hour>\d{1,2})(?P<minsep>:)?(?P<minute>\d{1,2})?(?P<secsep>:)?(?P<second>\d{1,2})?" # HH:mm:ss (optional mm and ss)
+ # Subsecond part (optional)
+ " (?P<subsecondsection>"
+ " (?:[.,])" # Subsecond separator (optional)
+ r" (?P<subsecond>\d{1,9})" # Subsecond
+ " )?"
+ # Timezone offset
+ " (?P<tz>"
+ r" (?:[-+])\d{2}:?(?:\d{2})?|Z" # Offset (+HH:mm or +HHmm or +HH or Z)
+ " )?"
+ ")?"
+ "$",
+ re.VERBOSE,
+)
+
+ISO8601_DURATION = re.compile(
+ "^P" # Duration P indicator
+ # Years, months and days (optional) # noqa: E800
+ "(?P<w>"
+ r" (?P<weeks>\d+(?:[.,]\d+)?W)"
+ ")?"
+ "(?P<ymd>"
+ r" (?P<years>\d+(?:[.,]\d+)?Y)?"
+ r" (?P<months>\d+(?:[.,]\d+)?M)?"
+ r" (?P<days>\d+(?:[.,]\d+)?D)?"
+ ")?"
+ "(?P<hms>"
+ " (?P<timesep>T)" # Separator (T)
+ r" (?P<hours>\d+(?:[.,]\d+)?H)?"
+ r" (?P<minutes>\d+(?:[.,]\d+)?M)?"
+ r" (?P<seconds>\d+(?:[.,]\d+)?S)?"
+ ")?"
+ "$",
+ re.VERBOSE,
+)
+
+
+def parse_iso8601(
+ text: str,
+) -> datetime.datetime | datetime.date | datetime.time | Duration:
+ """
+ ISO 8601 compliant parser.
+
+ :param text: The string to parse
+ :type text: str
+
+ :rtype: datetime.datetime or datetime.time or datetime.date
+ """
+ parsed = _parse_iso8601_duration(text)
+ if parsed is not None:
+ return parsed
+
+ m = ISO8601_DT.match(text)
+ if not m:
+ raise ParserError("Invalid ISO 8601 string")
+
+ ambiguous_date = False
+ is_date = False
+ is_time = False
+ year = 0
+ month = 1
+ day = 1
+ minute = 0
+ second = 0
+ microsecond = 0
+ tzinfo: FixedTimezone | None = None
+
+ if m.group("date"):
+ # A date has been specified
+ is_date = True
+
+ if m.group("isocalendar"):
+ # We have a ISO 8601 string defined
+ # by week number
+ if (
+ m.group("weeksep")
+ and not m.group("weekdaysep")
+ and m.group("isoweekday")
+ ):
+ raise ParserError(f"Invalid date string: {text}")
+
+ if not m.group("weeksep") and m.group("weekdaysep"):
+ raise ParserError(f"Invalid date string: {text}")
+
+ try:
+ date = _get_iso_8601_week(
+ m.group("isoyear"), m.group("isoweek"), m.group("isoweekday")
+ )
+ except ParserError:
+ raise
+ except ValueError:
+ raise ParserError(f"Invalid date string: {text}")
+
+ year = date["year"]
+ month = date["month"]
+ day = date["day"]
+ else:
+ # We have a classic date representation
+ year = int(m.group("year"))
+
+ if not m.group("monthday"):
+ # No month and day
+ month = 1
+ day = 1
+ else:
+ if m.group("month") and m.group("day"):
+ # Month and day
+ if not m.group("daysep") and len(m.group("day")) == 1:
+ # Ordinal day
+ ordinal = int(m.group("month") + m.group("day"))
+ leap = is_leap(year)
+ months_offsets = MONTHS_OFFSETS[leap]
+
+ if ordinal > months_offsets[13]:
+ raise ParserError("Ordinal day is out of range")
+
+ for i in range(1, 14):
+ if ordinal <= months_offsets[i]:
+ day = ordinal - months_offsets[i - 1]
+ month = i - 1
+
+ break
+ else:
+ month = int(m.group("month"))
+ day = int(m.group("day"))
+ else:
+ # Only month
+ if not m.group("monthsep"):
+ # The date looks like 201207
+ # which is invalid for a date
+ # But it might be a time in the form hhmmss
+ ambiguous_date = True
+
+ month = int(m.group("month"))
+ day = 1
+
+ if not m.group("time"):
+ # No time has been specified
+ if ambiguous_date:
+ # We can "safely" assume that the ambiguous date
+ # was actually a time in the form hhmmss
+ hhmmss = f"{str(year)}{str(month):0>2}"
+
+ return datetime.time(int(hhmmss[:2]), int(hhmmss[2:4]), int(hhmmss[4:]))
+
+ return datetime.date(year, month, day)
+
+ if ambiguous_date:
+ raise ParserError(f"Invalid date string: {text}")
+
+ if is_date and not m.group("timesep"):
+ raise ParserError(f"Invalid date string: {text}")
+
+ if not is_date:
+ is_time = True
+
+ # Grabbing hh:mm:ss
+ hour = int(m.group("hour"))
+ minsep = m.group("minsep")
+
+ if m.group("minute"):
+ minute = int(m.group("minute"))
+ elif minsep:
+ raise ParserError("Invalid ISO 8601 time part")
+
+ secsep = m.group("secsep")
+ if secsep and not minsep and m.group("minute"):
+ # minute/second separator but no hour/minute separator
+ raise ParserError("Invalid ISO 8601 time part")
+
+ if m.group("second"):
+ if not secsep and minsep:
+ # No minute/second separator but hour/minute separator
+ raise ParserError("Invalid ISO 8601 time part")
+
+ second = int(m.group("second"))
+ elif secsep:
+ raise ParserError("Invalid ISO 8601 time part")
+
+ # Grabbing subseconds, if any
+ if m.group("subsecondsection"):
+ # Limiting to 6 chars
+ subsecond = m.group("subsecond")[:6]
+
+ microsecond = int(f"{subsecond:0<6}")
+
+ # Grabbing timezone, if any
+ tz = m.group("tz")
+ if tz:
+ if tz == "Z":
+ tzinfo = UTC
+ else:
+ negative = bool(tz.startswith("-"))
+ tz = tz[1:]
+ if ":" not in tz:
+ if len(tz) == 2:
+ tz = f"{tz}00"
+
+ off_hour = tz[0:2]
+ off_minute = tz[2:4]
+ else:
+ off_hour, off_minute = tz.split(":")
+
+ offset = ((int(off_hour) * 60) + int(off_minute)) * 60
+
+ if negative:
+ offset = -1 * offset
+
+ tzinfo = FixedTimezone(offset)
+
+ if is_time:
+ return datetime.time(hour, minute, second, microsecond)
+
+ return datetime.datetime(
+ year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo
+ )
+
+
+def _parse_iso8601_duration(text: str, **options: str) -> Duration | None:
+ m = ISO8601_DURATION.match(text)
+ if not m:
+ return None
+
+ years = 0
+ months = 0
+ weeks = 0
+ days: int | float = 0
+ hours: int | float = 0
+ minutes: int | float = 0
+ seconds: int | float = 0
+ microseconds: int | float = 0
+ fractional = False
+
+ _days: str | float
+ _hour: str | int | None
+ _minutes: str | int | None
+ _seconds: str | int | None
+ if m.group("w"):
+ # Weeks
+ if m.group("ymd") or m.group("hms"):
+ # Specifying anything more than weeks is not supported
+ raise ParserError("Invalid duration string")
+
+ _weeks = m.group("weeks")
+ if not _weeks:
+ raise ParserError("Invalid duration string")
+
+ _weeks = _weeks.replace(",", ".").replace("W", "")
+ if "." in _weeks:
+ _weeks, portion = _weeks.split(".")
+ weeks = int(_weeks)
+ _days = int(portion) / 10 * 7
+ days, hours = int(_days // 1), int(_days % 1 * HOURS_PER_DAY)
+ else:
+ weeks = int(_weeks)
+
+ if m.group("ymd"):
+ # Years, months and/or days
+ _years = m.group("years")
+ _months = m.group("months")
+ _days = m.group("days")
+
+ # Checking order
+ years_start = m.start("years") if _years else -3
+ months_start = m.start("months") if _months else years_start + 1
+ days_start = m.start("days") if _days else months_start + 1
+
+ # Check correct order
+ if not (years_start < months_start < days_start):
+ raise ParserError("Invalid duration")
+
+ if _years:
+ _years = _years.replace(",", ".").replace("Y", "")
+ if "." in _years:
+ raise ParserError("Float years in duration are not supported")
+ else:
+ years = int(_years)
+
+ if _months:
+ if fractional:
+ raise ParserError("Invalid duration")
+
+ _months = _months.replace(",", ".").replace("M", "")
+ if "." in _months:
+ raise ParserError("Float months in duration are not supported")
+ else:
+ months = int(_months)
+
+ if _days:
+ if fractional:
+ raise ParserError("Invalid duration")
+
+ _days = _days.replace(",", ".").replace("D", "")
+
+ if "." in _days:
+ fractional = True
+
+ _days, _hours = _days.split(".")
+ days = int(_days)
+ hours = int(_hours) / 10 * HOURS_PER_DAY
+ else:
+ days = int(_days)
+
+ if m.group("hms"):
+ # Hours, minutes and/or seconds
+ _hours = m.group("hours") or 0
+ _minutes = m.group("minutes") or 0
+ _seconds = m.group("seconds") or 0
+
+ # Checking order
+ hours_start = m.start("hours") if _hours else -3
+ minutes_start = m.start("minutes") if _minutes else hours_start + 1
+ seconds_start = m.start("seconds") if _seconds else minutes_start + 1
+
+ # Check correct order
+ if not (hours_start < minutes_start < seconds_start):
+ raise ParserError("Invalid duration")
+
+ if _hours:
+ if fractional:
+ raise ParserError("Invalid duration")
+
+ _hours = cast(str, _hours).replace(",", ".").replace("H", "")
+
+ if "." in _hours:
+ fractional = True
+
+ _hours, _mins = _hours.split(".")
+ hours += int(_hours)
+ minutes += int(_mins) / 10 * MINUTES_PER_HOUR
+ else:
+ hours += int(_hours)
+
+ if _minutes:
+ if fractional:
+ raise ParserError("Invalid duration")
+
+ _minutes = cast(str, _minutes).replace(",", ".").replace("M", "")
+
+ if "." in _minutes:
+ fractional = True
+
+ _minutes, _secs = _minutes.split(".")
+ minutes += int(_minutes)
+ seconds += int(_secs) / 10 * SECONDS_PER_MINUTE
+ else:
+ minutes += int(_minutes)
+
+ if _seconds:
+ if fractional:
+ raise ParserError("Invalid duration")
+
+ _seconds = cast(str, _seconds).replace(",", ".").replace("S", "")
+
+ if "." in _seconds:
+ _seconds, _microseconds = _seconds.split(".")
+ seconds += int(_seconds)
+ microseconds += int(f"{_microseconds[:6]:0<6}")
+ else:
+ seconds += int(_seconds)
+
+ return Duration(
+ years=years,
+ months=months,
+ weeks=weeks,
+ days=days,
+ hours=hours,
+ minutes=minutes,
+ seconds=seconds,
+ microseconds=microseconds,
+ )
+
+
+def _get_iso_8601_week(
+ year: int | str, week: int | str, weekday: int | str
+) -> dict[str, int]:
+ if not weekday:
+ weekday = 1
+ else:
+ weekday = int(weekday)
+
+ year = int(year)
+ week = int(week)
+
+ if week > 53 or week > 52 and not is_long_year(year):
+ raise ParserError("Invalid week for week date")
+
+ if weekday > 7:
+ raise ParserError("Invalid weekday for week date")
+
+ # We can't rely on strptime directly here since
+ # it does not support ISO week date
+ ordinal = week * 7 + weekday - (week_day(year, 1, 4) + 3)
+
+ if ordinal < 1:
+ # Previous year
+ ordinal += days_in_year(year - 1)
+ year -= 1
+
+ if ordinal > days_in_year(year):
+ # Next year
+ ordinal -= days_in_year(year)
+ year += 1
+
+ fmt = "%Y-%j"
+ string = f"{year}-{ordinal}"
+
+ dt = datetime.datetime.strptime(string, fmt)
+
+ return {"year": dt.year, "month": dt.month, "day": dt.day}
diff --git a/pendulum/period.py b/pendulum/period.py
deleted file mode 100644
index b734b5b..0000000
--- a/pendulum/period.py
+++ /dev/null
@@ -1,390 +0,0 @@
-from __future__ import absolute_import
-
-import operator
-
-from datetime import date
-from datetime import datetime
-from datetime import timedelta
-
-import pendulum
-
-from pendulum.utils._compat import _HAS_FOLD
-from pendulum.utils._compat import decode
-
-from .constants import MONTHS_PER_YEAR
-from .duration import Duration
-from .helpers import precise_diff
-
-
-class Period(Duration):
- """
- Duration class that is aware of the datetimes that generated the
- time difference.
- """
-
- def __new__(cls, start, end, absolute=False):
- if isinstance(start, datetime) and isinstance(end, datetime):
- if (
- start.tzinfo is None
- and end.tzinfo is not None
- or start.tzinfo is not None
- and end.tzinfo is None
- ):
- raise TypeError("can't compare offset-naive and offset-aware datetimes")
-
- if absolute and start > end:
- end, start = start, end
-
- _start = start
- _end = end
- if isinstance(start, pendulum.DateTime):
- if _HAS_FOLD:
- _start = datetime(
- start.year,
- start.month,
- start.day,
- start.hour,
- start.minute,
- start.second,
- start.microsecond,
- tzinfo=start.tzinfo,
- fold=start.fold,
- )
- else:
- _start = datetime(
- start.year,
- start.month,
- start.day,
- start.hour,
- start.minute,
- start.second,
- start.microsecond,
- tzinfo=start.tzinfo,
- )
- elif isinstance(start, pendulum.Date):
- _start = date(start.year, start.month, start.day)
-
- if isinstance(end, pendulum.DateTime):
- if _HAS_FOLD:
- _end = datetime(
- end.year,
- end.month,
- end.day,
- end.hour,
- end.minute,
- end.second,
- end.microsecond,
- tzinfo=end.tzinfo,
- fold=end.fold,
- )
- else:
- _end = datetime(
- end.year,
- end.month,
- end.day,
- end.hour,
- end.minute,
- end.second,
- end.microsecond,
- tzinfo=end.tzinfo,
- )
- elif isinstance(end, pendulum.Date):
- _end = date(end.year, end.month, end.day)
-
- # Fixing issues with datetime.__sub__()
- # not handling offsets if the tzinfo is the same
- if (
- isinstance(_start, datetime)
- and isinstance(_end, datetime)
- and _start.tzinfo is _end.tzinfo
- ):
- if _start.tzinfo is not None:
- _start = (_start - start.utcoffset()).replace(tzinfo=None)
-
- if isinstance(end, datetime) and _end.tzinfo is not None:
- _end = (_end - end.utcoffset()).replace(tzinfo=None)
-
- delta = _end - _start
-
- return super(Period, cls).__new__(cls, seconds=delta.total_seconds())
-
- def __init__(self, start, end, absolute=False):
- super(Period, self).__init__()
-
- if not isinstance(start, pendulum.Date):
- if isinstance(start, datetime):
- start = pendulum.instance(start)
- else:
- start = pendulum.date(start.year, start.month, start.day)
-
- _start = start
- else:
- if isinstance(start, pendulum.DateTime):
- _start = datetime(
- start.year,
- start.month,
- start.day,
- start.hour,
- start.minute,
- start.second,
- start.microsecond,
- tzinfo=start.tzinfo,
- )
- else:
- _start = date(start.year, start.month, start.day)
-
- if not isinstance(end, pendulum.Date):
- if isinstance(end, datetime):
- end = pendulum.instance(end)
- else:
- end = pendulum.date(end.year, end.month, end.day)
-
- _end = end
- else:
- if isinstance(end, pendulum.DateTime):
- _end = datetime(
- end.year,
- end.month,
- end.day,
- end.hour,
- end.minute,
- end.second,
- end.microsecond,
- tzinfo=end.tzinfo,
- )
- else:
- _end = date(end.year, end.month, end.day)
-
- self._invert = False
- if start > end:
- self._invert = True
-
- if absolute:
- end, start = start, end
- _end, _start = _start, _end
-
- self._absolute = absolute
- self._start = start
- self._end = end
- self._delta = precise_diff(_start, _end)
-
- @property
- def years(self):
- return self._delta.years
-
- @property
- def months(self):
- return self._delta.months
-
- @property
- def weeks(self):
- return abs(self._delta.days) // 7 * self._sign(self._delta.days)
-
- @property
- def days(self):
- return self._days
-
- @property
- def remaining_days(self):
- return abs(self._delta.days) % 7 * self._sign(self._days)
-
- @property
- def hours(self):
- return self._delta.hours
-
- @property
- def minutes(self):
- return self._delta.minutes
-
- @property
- def start(self):
- return self._start
-
- @property
- def end(self):
- return self._end
-
- def in_years(self):
- """
- Gives the duration of the Period in full years.
-
- :rtype: int
- """
- return self.years
-
- def in_months(self):
- """
- Gives the duration of the Period in full months.
-
- :rtype: int
- """
- return self.years * MONTHS_PER_YEAR + self.months
-
- def in_weeks(self):
- days = self.in_days()
- sign = 1
-
- if days < 0:
- sign = -1
-
- return sign * (abs(days) // 7)
-
- def in_days(self):
- return self._delta.total_days
-
- def in_words(self, locale=None, separator=" "):
- """
- Get the current interval in words in the current locale.
-
- Ex: 6 jours 23 heures 58 minutes
-
- :param locale: The locale to use. Defaults to current locale.
- :type locale: str
-
- :param separator: The separator to use between each unit
- :type separator: str
-
- :rtype: str
- """
- periods = [
- ("year", self.years),
- ("month", self.months),
- ("week", self.weeks),
- ("day", self.remaining_days),
- ("hour", self.hours),
- ("minute", self.minutes),
- ("second", self.remaining_seconds),
- ]
-
- if locale is None:
- locale = pendulum.get_locale()
-
- locale = pendulum.locale(locale)
- parts = []
- for period in periods:
- unit, count = period
- if abs(count) > 0:
- translation = locale.translation(
- "units.{}.{}".format(unit, locale.plural(abs(count)))
- )
- parts.append(translation.format(count))
-
- if not parts:
- if abs(self.microseconds) > 0:
- unit = "units.second.{}".format(locale.plural(1))
- count = "{:.2f}".format(abs(self.microseconds) / 1e6)
- else:
- unit = "units.microsecond.{}".format(locale.plural(0))
- count = 0
- translation = locale.translation(unit)
- parts.append(translation.format(count))
-
- return decode(separator.join(parts))
-
- def range(self, unit, amount=1):
- method = "add"
- op = operator.le
- if not self._absolute and self.invert:
- method = "subtract"
- op = operator.ge
-
- start, end = self.start, self.end
-
- i = amount
- while op(start, end):
- yield start
-
- start = getattr(self.start, method)(**{unit: i})
-
- i += amount
-
- def as_interval(self):
- """
- Return the Period as an Duration.
-
- :rtype: Duration
- """
- return Duration(seconds=self.total_seconds())
-
- def __iter__(self):
- return self.range("days")
-
- def __contains__(self, item):
- return self.start <= item <= self.end
-
- def __add__(self, other):
- return self.as_interval().__add__(other)
-
- __radd__ = __add__
-
- def __sub__(self, other):
- return self.as_interval().__sub__(other)
-
- def __neg__(self):
- return self.__class__(self.end, self.start, self._absolute)
-
- def __mul__(self, other):
- return self.as_interval().__mul__(other)
-
- __rmul__ = __mul__
-
- def __floordiv__(self, other):
- return self.as_interval().__floordiv__(other)
-
- def __truediv__(self, other):
- return self.as_interval().__truediv__(other)
-
- __div__ = __floordiv__
-
- def __mod__(self, other):
- return self.as_interval().__mod__(other)
-
- def __divmod__(self, other):
- return self.as_interval().__divmod__(other)
-
- def __abs__(self):
- return self.__class__(self.start, self.end, True)
-
- def __repr__(self):
- return "<Period [{} -> {}]>".format(self._start, self._end)
-
- def __str__(self):
- return self.__repr__()
-
- def _cmp(self, other):
- # Only needed for PyPy
- assert isinstance(other, timedelta)
-
- if isinstance(other, Period):
- other = other.as_timedelta()
-
- td = self.as_timedelta()
-
- return 0 if td == other else 1 if td > other else -1
-
- def _getstate(self, protocol=3):
- start, end = self.start, self.end
-
- if self._invert and self._absolute:
- end, start = start, end
-
- return (start, end, self._absolute)
-
- def __reduce__(self):
- return self.__reduce_ex__(2)
-
- def __reduce_ex__(self, protocol):
- return self.__class__, self._getstate(protocol)
-
- def __hash__(self):
- return hash((self.start, self.end, self._absolute))
-
- def __eq__(self, other):
- if isinstance(other, Period):
- return (self.start, self.end, self._absolute) == (
- other.start,
- other.end,
- other._absolute,
- )
- else:
- return self.as_interval() == other
diff --git a/pendulum/testing/__init__.py b/pendulum/testing/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pendulum/testing/__init__.py
diff --git a/pendulum/testing/traveller.py b/pendulum/testing/traveller.py
new file mode 100644
index 0000000..3c1d885
--- /dev/null
+++ b/pendulum/testing/traveller.py
@@ -0,0 +1,139 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from typing import cast
+
+from pendulum.datetime import DateTime
+from pendulum.utils._compat import PYPY
+
+if TYPE_CHECKING:
+ from types import TracebackType
+
+
+class BaseTraveller:
+ def __init__(self, datetime_class: type[DateTime] = DateTime) -> None:
+ self._datetime_class: type[DateTime] = datetime_class
+
+ def freeze(self: BaseTraveller) -> BaseTraveller:
+ raise NotImplementedError()
+
+ def travel_back(self: BaseTraveller) -> BaseTraveller:
+ raise NotImplementedError()
+
+ def travel(
+ self,
+ years: int = 0,
+ months: int = 0,
+ weeks: int = 0,
+ days: int = 0,
+ hours: int = 0,
+ minutes: int = 0,
+ seconds: int = 0,
+ microseconds: int = 0,
+ ) -> BaseTraveller:
+ raise NotImplementedError()
+
+ def travel_to(self, dt: DateTime) -> BaseTraveller:
+ raise NotImplementedError()
+
+
+if not PYPY:
+ import time_machine
+
+ class Traveller(BaseTraveller):
+ def __init__(self, datetime_class: type[DateTime] = DateTime) -> None:
+ super().__init__(datetime_class)
+
+ self._started: bool = False
+ self._traveller: time_machine.travel | None = None
+ self._coordinates: time_machine.Coordinates | None = None
+
+ def freeze(self) -> Traveller:
+ if self._started:
+ cast(time_machine.Coordinates, self._coordinates).move_to(
+ self._datetime_class.now(), tick=False
+ )
+ else:
+ self._start(freeze=True)
+
+ return self
+
+ def travel_back(self) -> Traveller:
+ if not self._started:
+ return self
+
+ cast(time_machine.travel, self._traveller).stop()
+ self._coordinates = None
+ self._traveller = None
+ self._started = False
+
+ return self
+
+ def travel(
+ self,
+ years: int = 0,
+ months: int = 0,
+ weeks: int = 0,
+ days: int = 0,
+ hours: int = 0,
+ minutes: int = 0,
+ seconds: int = 0,
+ microseconds: int = 0,
+ *,
+ freeze: bool = False,
+ ) -> Traveller:
+ self._start(freeze=freeze)
+
+ cast(time_machine.Coordinates, self._coordinates).move_to(
+ self._datetime_class.now().add(
+ years=years,
+ months=months,
+ weeks=weeks,
+ days=days,
+ hours=hours,
+ minutes=minutes,
+ seconds=seconds,
+ microseconds=microseconds,
+ )
+ )
+
+ return self
+
+ def travel_to(self, dt: DateTime, *, freeze: bool = False) -> Traveller:
+ self._start(freeze=freeze)
+
+ cast(time_machine.Coordinates, self._coordinates).move_to(dt)
+
+ return self
+
+ def _start(self, freeze: bool = False) -> None:
+ if self._started:
+ return
+
+ if not self._traveller:
+ self._traveller = time_machine.travel(
+ self._datetime_class.now(), tick=not freeze
+ )
+
+ self._coordinates = self._traveller.start()
+
+ self._started = True
+
+ def __enter__(self) -> Traveller:
+ self._start()
+
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType,
+ ) -> None:
+ self.travel_back()
+
+else:
+
+ class Traveller(BaseTraveller): # type: ignore[no-redef]
+
+ ...
diff --git a/pendulum/time.py b/pendulum/time.py
index e72972d..f979e25 100644
--- a/pendulum/time.py
+++ b/pendulum/time.py
@@ -1,284 +1,303 @@
-from __future__ import absolute_import
-
-from datetime import time
-from datetime import timedelta
-
-import pendulum
-
-from .constants import SECS_PER_HOUR
-from .constants import SECS_PER_MIN
-from .constants import USECS_PER_SEC
-from .duration import AbsoluteDuration
-from .duration import Duration
-from .mixins.default import FormattableMixin
-
-
-class Time(FormattableMixin, time):
- """
- Represents a time instance as hour, minute, second, microsecond.
- """
-
- # String formatting
- def __repr__(self):
- us = ""
- if self.microsecond:
- us = ", {}".format(self.microsecond)
-
- tzinfo = ""
- if self.tzinfo:
- tzinfo = ", tzinfo={}".format(repr(self.tzinfo))
-
- return "{}({}, {}, {}{}{})".format(
- self.__class__.__name__, self.hour, self.minute, self.second, us, tzinfo
- )
-
- # Comparisons
-
- def closest(self, dt1, dt2):
- """
- Get the closest time from the instance.
-
- :type dt1: Time or time
- :type dt2: Time or time
-
- :rtype: Time
- """
- dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond)
- dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond)
-
- if self.diff(dt1).in_seconds() < self.diff(dt2).in_seconds():
- return dt1
-
- return dt2
-
- def farthest(self, dt1, dt2):
- """
- Get the farthest time from the instance.
-
- :type dt1: Time or time
- :type dt2: Time or time
-
- :rtype: Time
- """
- dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond)
- dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond)
-
- if self.diff(dt1).in_seconds() > self.diff(dt2).in_seconds():
- return dt1
-
- return dt2
-
- # ADDITIONS AND SUBSTRACTIONS
-
- def add(self, hours=0, minutes=0, seconds=0, microseconds=0):
- """
- Add duration to the instance.
-
- :param hours: The number of hours
- :type hours: int
-
- :param minutes: The number of minutes
- :type minutes: int
-
- :param seconds: The number of seconds
- :type seconds: int
-
- :param microseconds: The number of microseconds
- :type microseconds: int
-
- :rtype: Time
- """
- from .datetime import DateTime
-
- return (
- DateTime.EPOCH.at(self.hour, self.minute, self.second, self.microsecond)
- .add(
- hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds
- )
- .time()
- )
-
- def subtract(self, hours=0, minutes=0, seconds=0, microseconds=0):
- """
- Add duration to the instance.
-
- :param hours: The number of hours
- :type hours: int
-
- :param minutes: The number of minutes
- :type minutes: int
-
- :param seconds: The number of seconds
- :type seconds: int
-
- :param microseconds: The number of microseconds
- :type microseconds: int
-
- :rtype: Time
- """
- from .datetime import DateTime
-
- return (
- DateTime.EPOCH.at(self.hour, self.minute, self.second, self.microsecond)
- .subtract(
- hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds
- )
- .time()
- )
-
- def add_timedelta(self, delta):
- """
- Add timedelta duration to the instance.
-
- :param delta: The timedelta instance
- :type delta: datetime.timedelta
-
- :rtype: Time
- """
- if delta.days:
- raise TypeError("Cannot add timedelta with days to Time.")
-
- return self.add(seconds=delta.seconds, microseconds=delta.microseconds)
-
- def subtract_timedelta(self, delta):
- """
- Remove timedelta duration from the instance.
-
- :param delta: The timedelta instance
- :type delta: datetime.timedelta
-
- :rtype: Time
- """
- if delta.days:
- raise TypeError("Cannot subtract timedelta with days to Time.")
-
- return self.subtract(seconds=delta.seconds, microseconds=delta.microseconds)
-
- def __add__(self, other):
- if not isinstance(other, timedelta):
- return NotImplemented
-
- return self.add_timedelta(other)
-
- def __sub__(self, other):
- if not isinstance(other, (Time, time, timedelta)):
- return NotImplemented
-
- if isinstance(other, timedelta):
- return self.subtract_timedelta(other)
-
- if isinstance(other, time):
- if other.tzinfo is not None:
- raise TypeError("Cannot subtract aware times to or from Time.")
-
- other = self.__class__(
- other.hour, other.minute, other.second, other.microsecond
- )
-
- return other.diff(self, False)
-
- def __rsub__(self, other):
- if not isinstance(other, (Time, time)):
- return NotImplemented
-
- if isinstance(other, time):
- if other.tzinfo is not None:
- raise TypeError("Cannot subtract aware times to or from Time.")
-
- other = self.__class__(
- other.hour, other.minute, other.second, other.microsecond
- )
-
- return other.__sub__(self)
-
- # DIFFERENCES
-
- def diff(self, dt=None, abs=True):
- """
- Returns the difference between two Time objects as an Duration.
-
- :type dt: Time or None
-
- :param abs: Whether to return an absolute interval or not
- :type abs: bool
-
- :rtype: Duration
- """
- if dt is None:
- dt = pendulum.now().time()
- else:
- dt = self.__class__(dt.hour, dt.minute, dt.second, dt.microsecond)
-
- us1 = (
- self.hour * SECS_PER_HOUR + self.minute * SECS_PER_MIN + self.second
- ) * USECS_PER_SEC
-
- us2 = (
- dt.hour * SECS_PER_HOUR + dt.minute * SECS_PER_MIN + dt.second
- ) * USECS_PER_SEC
-
- klass = Duration
- if abs:
- klass = AbsoluteDuration
-
- return klass(microseconds=us2 - us1)
-
- def diff_for_humans(self, other=None, absolute=False, locale=None):
- """
- Get the difference in a human readable format in the current locale.
-
- :type other: Time or time
-
- :param absolute: removes time difference modifiers ago, after, etc
- :type absolute: bool
-
- :param locale: The locale to use for localization
- :type locale: str
-
- :rtype: str
- """
- is_now = other is None
-
- if is_now:
- other = pendulum.now().time()
-
- diff = self.diff(other)
-
- return pendulum.format_diff(diff, is_now, absolute, locale)
-
- # Compatibility methods
-
- def replace(
- self, hour=None, minute=None, second=None, microsecond=None, tzinfo=True
- ):
- if tzinfo is True:
- tzinfo = self.tzinfo
-
- hour = hour if hour is not None else self.hour
- minute = minute if minute is not None else self.minute
- second = second if second is not None else self.second
- microsecond = microsecond if microsecond is not None else self.microsecond
-
- t = super(Time, self).replace(hour, minute, second, microsecond, tzinfo=tzinfo)
- return self.__class__(
- t.hour, t.minute, t.second, t.microsecond, tzinfo=t.tzinfo
- )
-
- def __getnewargs__(self):
- return (self,)
-
- def _get_state(self, protocol=3):
- tz = self.tzinfo
-
- return (self.hour, self.minute, self.second, self.microsecond, tz)
-
- def __reduce__(self):
- return self.__reduce_ex__(2)
-
- def __reduce_ex__(self, protocol):
- return self.__class__, self._get_state(protocol)
-
-
-Time.min = Time(0, 0, 0)
-Time.max = Time(23, 59, 59, 999999)
-Time.resolution = Duration(microseconds=1)
+from __future__ import annotations
+
+import datetime
+
+from datetime import time
+from datetime import timedelta
+from typing import TYPE_CHECKING
+from typing import Optional
+from typing import cast
+from typing import overload
+
+import pendulum
+
+from pendulum.constants import SECS_PER_HOUR
+from pendulum.constants import SECS_PER_MIN
+from pendulum.constants import USECS_PER_SEC
+from pendulum.duration import AbsoluteDuration
+from pendulum.duration import Duration
+from pendulum.mixins.default import FormattableMixin
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+
+class Time(FormattableMixin, time):
+ """
+ Represents a time instance as hour, minute, second, microsecond.
+ """
+
+ # String formatting
+ def __repr__(self) -> str:
+ us = ""
+ if self.microsecond:
+ us = f", {self.microsecond}"
+
+ tzinfo = ""
+ if self.tzinfo:
+ tzinfo = f", tzinfo={repr(self.tzinfo)}"
+
+ return (
+ f"{self.__class__.__name__}"
+ f"({self.hour}, {self.minute}, {self.second}{us}{tzinfo})"
+ )
+
+ # Comparisons
+
+ def closest(self, dt1: Time | time, dt2: Time | time) -> Time:
+ """
+ Get the closest time from the instance.
+ """
+ dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond)
+ dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond)
+
+ if self.diff(dt1).in_seconds() < self.diff(dt2).in_seconds():
+ return dt1
+
+ return dt2
+
+ def farthest(self, dt1: Time | time, dt2: Time | time) -> Time:
+ """
+ Get the farthest time from the instance.
+ """
+ dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond)
+ dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond)
+
+ if self.diff(dt1).in_seconds() > self.diff(dt2).in_seconds():
+ return dt1
+
+ return dt2
+
+ # ADDITIONS AND SUBSTRACTIONS
+
+ def add(
+ self, hours: int = 0, minutes: int = 0, seconds: int = 0, microseconds: int = 0
+ ) -> Time:
+ """
+ Add duration to the instance.
+
+ :param hours: The number of hours
+ :param minutes: The number of minutes
+ :param seconds: The number of seconds
+ :param microseconds: The number of microseconds
+ """
+ from pendulum.datetime import DateTime
+
+ return (
+ DateTime.EPOCH.at(self.hour, self.minute, self.second, self.microsecond)
+ .add(
+ hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds
+ )
+ .time()
+ )
+
+ def subtract(
+ self, hours: int = 0, minutes: int = 0, seconds: int = 0, microseconds: int = 0
+ ) -> Time:
+ """
+ Add duration to the instance.
+
+ :param hours: The number of hours
+ :type hours: int
+
+ :param minutes: The number of minutes
+ :type minutes: int
+
+ :param seconds: The number of seconds
+ :type seconds: int
+
+ :param microseconds: The number of microseconds
+ :type microseconds: int
+
+ :rtype: Time
+ """
+ from pendulum.datetime import DateTime
+
+ return (
+ DateTime.EPOCH.at(self.hour, self.minute, self.second, self.microsecond)
+ .subtract(
+ hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds
+ )
+ .time()
+ )
+
+ def add_timedelta(self, delta: datetime.timedelta) -> Time:
+ """
+ Add timedelta duration to the instance.
+
+ :param delta: The timedelta instance
+ """
+ if delta.days:
+ raise TypeError("Cannot add timedelta with days to Time.")
+
+ return self.add(seconds=delta.seconds, microseconds=delta.microseconds)
+
+ def subtract_timedelta(self, delta: datetime.timedelta) -> Time:
+ """
+ Remove timedelta duration from the instance.
+
+ :param delta: The timedelta instance
+ """
+ if delta.days:
+ raise TypeError("Cannot subtract timedelta with days to Time.")
+
+ return self.subtract(seconds=delta.seconds, microseconds=delta.microseconds)
+
+ def __add__(self, other: datetime.timedelta) -> Time:
+ if not isinstance(other, timedelta):
+ return NotImplemented
+
+ return self.add_timedelta(other)
+
+ @overload
+ def __sub__(self, other: time) -> pendulum.Duration:
+ ...
+
+ @overload
+ def __sub__(self, other: datetime.timedelta) -> Time:
+ ...
+
+ def __sub__(self, other: time | datetime.timedelta) -> pendulum.Duration | Time:
+ if not isinstance(other, (Time, time, timedelta)):
+ return NotImplemented
+
+ if isinstance(other, timedelta):
+ return self.subtract_timedelta(other)
+
+ if isinstance(other, time):
+ if other.tzinfo is not None:
+ raise TypeError("Cannot subtract aware times to or from Time.")
+
+ other = self.__class__(
+ other.hour, other.minute, other.second, other.microsecond
+ )
+
+ return other.diff(self, False)
+
+ @overload
+ def __rsub__(self, other: time) -> pendulum.Duration:
+ ...
+
+ @overload
+ def __rsub__(self, other: datetime.timedelta) -> Time:
+ ...
+
+ def __rsub__(self, other: time | datetime.timedelta) -> pendulum.Duration | Time:
+ if not isinstance(other, (Time, time)):
+ return NotImplemented
+
+ if isinstance(other, time):
+ if other.tzinfo is not None:
+ raise TypeError("Cannot subtract aware times to or from Time.")
+
+ other = self.__class__(
+ other.hour, other.minute, other.second, other.microsecond
+ )
+
+ return other.__sub__(self)
+
+ # DIFFERENCES
+
+ def diff(self, dt: time | None = None, abs: bool = True) -> Duration:
+ """
+ Returns the difference between two Time objects as an Duration.
+
+ :param dt: The time to subtract from
+ :param abs: Whether to return an absolute duration or not
+ """
+ if dt is None:
+ dt = pendulum.now().time()
+ else:
+ dt = self.__class__(dt.hour, dt.minute, dt.second, dt.microsecond)
+
+ us1 = (
+ self.hour * SECS_PER_HOUR + self.minute * SECS_PER_MIN + self.second
+ ) * USECS_PER_SEC
+
+ us2 = (
+ dt.hour * SECS_PER_HOUR + dt.minute * SECS_PER_MIN + dt.second
+ ) * USECS_PER_SEC
+
+ klass = Duration
+ if abs:
+ klass = AbsoluteDuration
+
+ return klass(microseconds=us2 - us1)
+
+ def diff_for_humans(
+ self,
+ other: time | None = None,
+ absolute: bool = False,
+ locale: str | None = None,
+ ) -> str:
+ """
+ Get the difference in a human readable format in the current locale.
+
+ :param dt: The time to subtract from
+ :param absolute: removes time difference modifiers ago, after, etc
+ :param locale: The locale to use for localization
+ """
+ is_now = other is None
+
+ if is_now:
+ other = pendulum.now().time()
+
+ diff = self.diff(other)
+
+ return pendulum.format_diff(diff, is_now, absolute, locale)
+
+ # Compatibility methods
+
+ def replace(
+ self,
+ hour: int | None = None,
+ minute: int | None = None,
+ second: int | None = None,
+ microsecond: int | None = None,
+ tzinfo: bool | datetime.tzinfo | Literal[True] | None = True,
+ fold: int = 0,
+ ) -> Time:
+ if tzinfo is True:
+ tzinfo = self.tzinfo
+
+ hour = hour if hour is not None else self.hour
+ minute = minute if minute is not None else self.minute
+ second = second if second is not None else self.second
+ microsecond = microsecond if microsecond is not None else self.microsecond
+
+ t = super().replace(
+ hour,
+ minute,
+ second,
+ microsecond,
+ tzinfo=cast(Optional[datetime.tzinfo], tzinfo),
+ fold=fold,
+ )
+ return self.__class__(
+ t.hour, t.minute, t.second, t.microsecond, tzinfo=t.tzinfo
+ )
+
+ def __getnewargs__(self) -> tuple[Time]:
+ return (self,)
+
+ def _get_state(
+ self, protocol: int = 3
+ ) -> tuple[int, int, int, int, datetime.tzinfo | None]:
+ tz = self.tzinfo
+
+ return self.hour, self.minute, self.second, self.microsecond, tz
+
+ def __reduce__(
+ self,
+ ) -> tuple[type[Time], tuple[int, int, int, int, datetime.tzinfo | None]]:
+ return self.__reduce_ex__(2)
+
+ def __reduce_ex__( # type: ignore[override]
+ self, protocol: int
+ ) -> tuple[type[Time], tuple[int, int, int, int, datetime.tzinfo | None]]:
+ return self.__class__, self._get_state(protocol)
+
+
+Time.min = Time(0, 0, 0)
+Time.max = Time(23, 59, 59, 999999)
+Time.resolution = Duration(microseconds=1)
diff --git a/pendulum/tz/__init__.py b/pendulum/tz/__init__.py
index b085f37..45c9855 100644
--- a/pendulum/tz/__init__.py
+++ b/pendulum/tz/__init__.py
@@ -1,60 +1,80 @@
-from typing import Tuple
-from typing import Union
-
-import pytzdata
-
-from .local_timezone import get_local_timezone
-from .local_timezone import set_local_timezone
-from .local_timezone import test_local_timezone
-from .timezone import UTC
-from .timezone import FixedTimezone as _FixedTimezone
-from .timezone import Timezone as _Timezone
-
-
-PRE_TRANSITION = "pre"
-POST_TRANSITION = "post"
-TRANSITION_ERROR = "error"
-
-timezones = pytzdata.timezones # type: Tuple[str, ...]
-
-
-_tz_cache = {}
-
-
-def timezone(name, extended=True): # type: (Union[str, int], bool) -> _Timezone
- """
- Return a Timezone instance given its name.
- """
- if isinstance(name, int):
- return fixed_timezone(name)
-
- if name.lower() == "utc":
- return UTC
-
- if name in _tz_cache:
- return _tz_cache[name]
-
- tz = _Timezone(name, extended=extended)
- _tz_cache[name] = tz
-
- return tz
-
-
-def fixed_timezone(offset): # type: (int) -> _FixedTimezone
- """
- Return a Timezone instance given its offset in seconds.
- """
- if offset in _tz_cache:
- return _tz_cache[offset] # type: ignore
-
- tz = _FixedTimezone(offset)
- _tz_cache[offset] = tz
-
- return tz
-
-
-def local_timezone(): # type: () -> _Timezone
- """
- Return the local timezone.
- """
- return get_local_timezone()
+from __future__ import annotations
+
+import sys
+
+from pendulum.tz.local_timezone import get_local_timezone
+from pendulum.tz.local_timezone import set_local_timezone
+from pendulum.tz.local_timezone import test_local_timezone
+from pendulum.tz.timezone import UTC
+from pendulum.tz.timezone import FixedTimezone
+from pendulum.tz.timezone import Timezone
+
+if sys.version_info >= (3, 9):
+ from importlib import resources
+else:
+ import importlib_resources as resources
+
+PRE_TRANSITION = "pre"
+POST_TRANSITION = "post"
+TRANSITION_ERROR = "error"
+
+_timezones = None
+
+_tz_cache: dict[int, FixedTimezone] = {}
+
+
+def timezones() -> tuple[str, ...]:
+ global _timezones
+
+ if _timezones is None:
+ with resources.files("tzdata").joinpath("zones").open() as f:
+ _timezones = tuple(tz.strip() for tz in f.readlines())
+
+ return _timezones
+
+
+def timezone(name: str | int) -> Timezone | FixedTimezone:
+ """
+ Return a Timezone instance given its name.
+ """
+ if isinstance(name, int):
+ return fixed_timezone(name)
+
+ if name.lower() == "utc":
+ return UTC
+
+ return Timezone(name)
+
+
+def fixed_timezone(offset: int) -> FixedTimezone:
+ """
+ Return a Timezone instance given its offset in seconds.
+ """
+ if offset in _tz_cache:
+ return _tz_cache[offset]
+
+ tz = FixedTimezone(offset)
+ _tz_cache[offset] = tz
+
+ return tz
+
+
+def local_timezone() -> Timezone | FixedTimezone:
+ """
+ Return the local timezone.
+ """
+ return get_local_timezone()
+
+
+__all__ = [
+ "UTC",
+ "Timezone",
+ "FixedTimezone",
+ "set_local_timezone",
+ "get_local_timezone",
+ "test_local_timezone",
+ "timezone",
+ "fixed_timezone",
+ "local_timezone",
+ "timezones",
+]
diff --git a/pendulum/tz/data/windows.py b/pendulum/tz/data/windows.py
index 7fb5b32..65aa6c3 100644
--- a/pendulum/tz/data/windows.py
+++ b/pendulum/tz/data/windows.py
@@ -1,137 +1,139 @@
-windows_timezones = {
- "AUS Central Standard Time": "Australia/Darwin",
- "AUS Eastern Standard Time": "Australia/Sydney",
- "Afghanistan Standard Time": "Asia/Kabul",
- "Alaskan Standard Time": "America/Anchorage",
- "Aleutian Standard Time": "America/Adak",
- "Altai Standard Time": "Asia/Barnaul",
- "Arab Standard Time": "Asia/Riyadh",
- "Arabian Standard Time": "Asia/Dubai",
- "Arabic Standard Time": "Asia/Baghdad",
- "Argentina Standard Time": "America/Buenos_Aires",
- "Astrakhan Standard Time": "Europe/Astrakhan",
- "Atlantic Standard Time": "America/Halifax",
- "Aus Central W. Standard Time": "Australia/Eucla",
- "Azerbaijan Standard Time": "Asia/Baku",
- "Azores Standard Time": "Atlantic/Azores",
- "Bahia Standard Time": "America/Bahia",
- "Bangladesh Standard Time": "Asia/Dhaka",
- "Belarus Standard Time": "Europe/Minsk",
- "Bougainville Standard Time": "Pacific/Bougainville",
- "Canada Central Standard Time": "America/Regina",
- "Cape Verde Standard Time": "Atlantic/Cape_Verde",
- "Caucasus Standard Time": "Asia/Yerevan",
- "Cen. Australia Standard Time": "Australia/Adelaide",
- "Central America Standard Time": "America/Guatemala",
- "Central Asia Standard Time": "Asia/Almaty",
- "Central Brazilian Standard Time": "America/Cuiaba",
- "Central Europe Standard Time": "Europe/Budapest",
- "Central European Standard Time": "Europe/Warsaw",
- "Central Pacific Standard Time": "Pacific/Guadalcanal",
- "Central Standard Time": "America/Chicago",
- "Central Standard Time (Mexico)": "America/Mexico_City",
- "Chatham Islands Standard Time": "Pacific/Chatham",
- "China Standard Time": "Asia/Shanghai",
- "Cuba Standard Time": "America/Havana",
- "Dateline Standard Time": "Etc/GMT+12",
- "E. Africa Standard Time": "Africa/Nairobi",
- "E. Australia Standard Time": "Australia/Brisbane",
- "E. Europe Standard Time": "Europe/Chisinau",
- "E. South America Standard Time": "America/Sao_Paulo",
- "Easter Island Standard Time": "Pacific/Easter",
- "Eastern Standard Time": "America/New_York",
- "Eastern Standard Time (Mexico)": "America/Cancun",
- "Egypt Standard Time": "Africa/Cairo",
- "Ekaterinburg Standard Time": "Asia/Yekaterinburg",
- "FLE Standard Time": "Europe/Kiev",
- "Fiji Standard Time": "Pacific/Fiji",
- "GMT Standard Time": "Europe/London",
- "GTB Standard Time": "Europe/Bucharest",
- "Georgian Standard Time": "Asia/Tbilisi",
- "Greenland Standard Time": "America/Godthab",
- "Greenwich Standard Time": "Atlantic/Reykjavik",
- "Haiti Standard Time": "America/Port-au-Prince",
- "Hawaiian Standard Time": "Pacific/Honolulu",
- "India Standard Time": "Asia/Calcutta",
- "Iran Standard Time": "Asia/Tehran",
- "Israel Standard Time": "Asia/Jerusalem",
- "Jordan Standard Time": "Asia/Amman",
- "Kaliningrad Standard Time": "Europe/Kaliningrad",
- "Korea Standard Time": "Asia/Seoul",
- "Libya Standard Time": "Africa/Tripoli",
- "Line Islands Standard Time": "Pacific/Kiritimati",
- "Lord Howe Standard Time": "Australia/Lord_Howe",
- "Magadan Standard Time": "Asia/Magadan",
- "Magallanes Standard Time": "America/Punta_Arenas",
- "Marquesas Standard Time": "Pacific/Marquesas",
- "Mauritius Standard Time": "Indian/Mauritius",
- "Middle East Standard Time": "Asia/Beirut",
- "Montevideo Standard Time": "America/Montevideo",
- "Morocco Standard Time": "Africa/Casablanca",
- "Mountain Standard Time": "America/Denver",
- "Mountain Standard Time (Mexico)": "America/Chihuahua",
- "Myanmar Standard Time": "Asia/Rangoon",
- "N. Central Asia Standard Time": "Asia/Novosibirsk",
- "Namibia Standard Time": "Africa/Windhoek",
- "Nepal Standard Time": "Asia/Katmandu",
- "New Zealand Standard Time": "Pacific/Auckland",
- "Newfoundland Standard Time": "America/St_Johns",
- "Norfolk Standard Time": "Pacific/Norfolk",
- "North Asia East Standard Time": "Asia/Irkutsk",
- "North Asia Standard Time": "Asia/Krasnoyarsk",
- "North Korea Standard Time": "Asia/Pyongyang",
- "Omsk Standard Time": "Asia/Omsk",
- "Pacific SA Standard Time": "America/Santiago",
- "Pacific Standard Time": "America/Los_Angeles",
- "Pacific Standard Time (Mexico)": "America/Tijuana",
- "Pakistan Standard Time": "Asia/Karachi",
- "Paraguay Standard Time": "America/Asuncion",
- "Romance Standard Time": "Europe/Paris",
- "Russia Time Zone 10": "Asia/Srednekolymsk",
- "Russia Time Zone 11": "Asia/Kamchatka",
- "Russia Time Zone 3": "Europe/Samara",
- "Russian Standard Time": "Europe/Moscow",
- "SA Eastern Standard Time": "America/Cayenne",
- "SA Pacific Standard Time": "America/Bogota",
- "SA Western Standard Time": "America/La_Paz",
- "SE Asia Standard Time": "Asia/Bangkok",
- "Saint Pierre Standard Time": "America/Miquelon",
- "Sakhalin Standard Time": "Asia/Sakhalin",
- "Samoa Standard Time": "Pacific/Apia",
- "Sao Tome Standard Time": "Africa/Sao_Tome",
- "Saratov Standard Time": "Europe/Saratov",
- "Singapore Standard Time": "Asia/Singapore",
- "South Africa Standard Time": "Africa/Johannesburg",
- "Sri Lanka Standard Time": "Asia/Colombo",
- "Sudan Standard Time": "Africa/Khartoum",
- "Syria Standard Time": "Asia/Damascus",
- "Taipei Standard Time": "Asia/Taipei",
- "Tasmania Standard Time": "Australia/Hobart",
- "Tocantins Standard Time": "America/Araguaina",
- "Tokyo Standard Time": "Asia/Tokyo",
- "Tomsk Standard Time": "Asia/Tomsk",
- "Tonga Standard Time": "Pacific/Tongatapu",
- "Transbaikal Standard Time": "Asia/Chita",
- "Turkey Standard Time": "Europe/Istanbul",
- "Turks And Caicos Standard Time": "America/Grand_Turk",
- "US Eastern Standard Time": "America/Indianapolis",
- "US Mountain Standard Time": "America/Phoenix",
- "UTC": "Etc/GMT",
- "UTC+12": "Etc/GMT-12",
- "UTC+13": "Etc/GMT-13",
- "UTC-02": "Etc/GMT+2",
- "UTC-08": "Etc/GMT+8",
- "UTC-09": "Etc/GMT+9",
- "UTC-11": "Etc/GMT+11",
- "Ulaanbaatar Standard Time": "Asia/Ulaanbaatar",
- "Venezuela Standard Time": "America/Caracas",
- "Vladivostok Standard Time": "Asia/Vladivostok",
- "W. Australia Standard Time": "Australia/Perth",
- "W. Central Africa Standard Time": "Africa/Lagos",
- "W. Europe Standard Time": "Europe/Berlin",
- "W. Mongolia Standard Time": "Asia/Hovd",
- "West Asia Standard Time": "Asia/Tashkent",
- "West Bank Standard Time": "Asia/Hebron",
- "West Pacific Standard Time": "Pacific/Port_Moresby",
- "Yakutsk Standard Time": "Asia/Yakutsk",
-}
+from __future__ import annotations
+
+windows_timezones = {
+ "AUS Central Standard Time": "Australia/Darwin",
+ "AUS Eastern Standard Time": "Australia/Sydney",
+ "Afghanistan Standard Time": "Asia/Kabul",
+ "Alaskan Standard Time": "America/Anchorage",
+ "Aleutian Standard Time": "America/Adak",
+ "Altai Standard Time": "Asia/Barnaul",
+ "Arab Standard Time": "Asia/Riyadh",
+ "Arabian Standard Time": "Asia/Dubai",
+ "Arabic Standard Time": "Asia/Baghdad",
+ "Argentina Standard Time": "America/Buenos_Aires",
+ "Astrakhan Standard Time": "Europe/Astrakhan",
+ "Atlantic Standard Time": "America/Halifax",
+ "Aus Central W. Standard Time": "Australia/Eucla",
+ "Azerbaijan Standard Time": "Asia/Baku",
+ "Azores Standard Time": "Atlantic/Azores",
+ "Bahia Standard Time": "America/Bahia",
+ "Bangladesh Standard Time": "Asia/Dhaka",
+ "Belarus Standard Time": "Europe/Minsk",
+ "Bougainville Standard Time": "Pacific/Bougainville",
+ "Canada Central Standard Time": "America/Regina",
+ "Cape Verde Standard Time": "Atlantic/Cape_Verde",
+ "Caucasus Standard Time": "Asia/Yerevan",
+ "Cen. Australia Standard Time": "Australia/Adelaide",
+ "Central America Standard Time": "America/Guatemala",
+ "Central Asia Standard Time": "Asia/Almaty",
+ "Central Brazilian Standard Time": "America/Cuiaba",
+ "Central Europe Standard Time": "Europe/Budapest",
+ "Central European Standard Time": "Europe/Warsaw",
+ "Central Pacific Standard Time": "Pacific/Guadalcanal",
+ "Central Standard Time": "America/Chicago",
+ "Central Standard Time (Mexico)": "America/Mexico_City",
+ "Chatham Islands Standard Time": "Pacific/Chatham",
+ "China Standard Time": "Asia/Shanghai",
+ "Cuba Standard Time": "America/Havana",
+ "Dateline Standard Time": "Etc/GMT+12",
+ "E. Africa Standard Time": "Africa/Nairobi",
+ "E. Australia Standard Time": "Australia/Brisbane",
+ "E. Europe Standard Time": "Europe/Chisinau",
+ "E. South America Standard Time": "America/Sao_Paulo",
+ "Easter Island Standard Time": "Pacific/Easter",
+ "Eastern Standard Time": "America/New_York",
+ "Eastern Standard Time (Mexico)": "America/Cancun",
+ "Egypt Standard Time": "Africa/Cairo",
+ "Ekaterinburg Standard Time": "Asia/Yekaterinburg",
+ "FLE Standard Time": "Europe/Kiev",
+ "Fiji Standard Time": "Pacific/Fiji",
+ "GMT Standard Time": "Europe/London",
+ "GTB Standard Time": "Europe/Bucharest",
+ "Georgian Standard Time": "Asia/Tbilisi",
+ "Greenland Standard Time": "America/Godthab",
+ "Greenwich Standard Time": "Atlantic/Reykjavik",
+ "Haiti Standard Time": "America/Port-au-Prince",
+ "Hawaiian Standard Time": "Pacific/Honolulu",
+ "India Standard Time": "Asia/Calcutta",
+ "Iran Standard Time": "Asia/Tehran",
+ "Israel Standard Time": "Asia/Jerusalem",
+ "Jordan Standard Time": "Asia/Amman",
+ "Kaliningrad Standard Time": "Europe/Kaliningrad",
+ "Korea Standard Time": "Asia/Seoul",
+ "Libya Standard Time": "Africa/Tripoli",
+ "Line Islands Standard Time": "Pacific/Kiritimati",
+ "Lord Howe Standard Time": "Australia/Lord_Howe",
+ "Magadan Standard Time": "Asia/Magadan",
+ "Magallanes Standard Time": "America/Punta_Arenas",
+ "Marquesas Standard Time": "Pacific/Marquesas",
+ "Mauritius Standard Time": "Indian/Mauritius",
+ "Middle East Standard Time": "Asia/Beirut",
+ "Montevideo Standard Time": "America/Montevideo",
+ "Morocco Standard Time": "Africa/Casablanca",
+ "Mountain Standard Time": "America/Denver",
+ "Mountain Standard Time (Mexico)": "America/Chihuahua",
+ "Myanmar Standard Time": "Asia/Rangoon",
+ "N. Central Asia Standard Time": "Asia/Novosibirsk",
+ "Namibia Standard Time": "Africa/Windhoek",
+ "Nepal Standard Time": "Asia/Katmandu",
+ "New Zealand Standard Time": "Pacific/Auckland",
+ "Newfoundland Standard Time": "America/St_Johns",
+ "Norfolk Standard Time": "Pacific/Norfolk",
+ "North Asia East Standard Time": "Asia/Irkutsk",
+ "North Asia Standard Time": "Asia/Krasnoyarsk",
+ "North Korea Standard Time": "Asia/Pyongyang",
+ "Omsk Standard Time": "Asia/Omsk",
+ "Pacific SA Standard Time": "America/Santiago",
+ "Pacific Standard Time": "America/Los_Angeles",
+ "Pacific Standard Time (Mexico)": "America/Tijuana",
+ "Pakistan Standard Time": "Asia/Karachi",
+ "Paraguay Standard Time": "America/Asuncion",
+ "Romance Standard Time": "Europe/Paris",
+ "Russia Time Zone 10": "Asia/Srednekolymsk",
+ "Russia Time Zone 11": "Asia/Kamchatka",
+ "Russia Time Zone 3": "Europe/Samara",
+ "Russian Standard Time": "Europe/Moscow",
+ "SA Eastern Standard Time": "America/Cayenne",
+ "SA Pacific Standard Time": "America/Bogota",
+ "SA Western Standard Time": "America/La_Paz",
+ "SE Asia Standard Time": "Asia/Bangkok",
+ "Saint Pierre Standard Time": "America/Miquelon",
+ "Sakhalin Standard Time": "Asia/Sakhalin",
+ "Samoa Standard Time": "Pacific/Apia",
+ "Sao Tome Standard Time": "Africa/Sao_Tome",
+ "Saratov Standard Time": "Europe/Saratov",
+ "Singapore Standard Time": "Asia/Singapore",
+ "South Africa Standard Time": "Africa/Johannesburg",
+ "Sri Lanka Standard Time": "Asia/Colombo",
+ "Sudan Standard Time": "Africa/Khartoum",
+ "Syria Standard Time": "Asia/Damascus",
+ "Taipei Standard Time": "Asia/Taipei",
+ "Tasmania Standard Time": "Australia/Hobart",
+ "Tocantins Standard Time": "America/Araguaina",
+ "Tokyo Standard Time": "Asia/Tokyo",
+ "Tomsk Standard Time": "Asia/Tomsk",
+ "Tonga Standard Time": "Pacific/Tongatapu",
+ "Transbaikal Standard Time": "Asia/Chita",
+ "Turkey Standard Time": "Europe/Istanbul",
+ "Turks And Caicos Standard Time": "America/Grand_Turk",
+ "US Eastern Standard Time": "America/Indianapolis",
+ "US Mountain Standard Time": "America/Phoenix",
+ "UTC": "Etc/GMT",
+ "UTC+12": "Etc/GMT-12",
+ "UTC+13": "Etc/GMT-13",
+ "UTC-02": "Etc/GMT+2",
+ "UTC-08": "Etc/GMT+8",
+ "UTC-09": "Etc/GMT+9",
+ "UTC-11": "Etc/GMT+11",
+ "Ulaanbaatar Standard Time": "Asia/Ulaanbaatar",
+ "Venezuela Standard Time": "America/Caracas",
+ "Vladivostok Standard Time": "Asia/Vladivostok",
+ "W. Australia Standard Time": "Australia/Perth",
+ "W. Central Africa Standard Time": "Africa/Lagos",
+ "W. Europe Standard Time": "Europe/Berlin",
+ "W. Mongolia Standard Time": "Asia/Hovd",
+ "West Asia Standard Time": "Asia/Tashkent",
+ "West Bank Standard Time": "Asia/Hebron",
+ "West Pacific Standard Time": "Pacific/Port_Moresby",
+ "Yakutsk Standard Time": "Asia/Yakutsk",
+}
diff --git a/pendulum/tz/exceptions.py b/pendulum/tz/exceptions.py
index d1572f9..b8833ac 100644
--- a/pendulum/tz/exceptions.py
+++ b/pendulum/tz/exceptions.py
@@ -1,23 +1,32 @@
-class TimezoneError(ValueError):
-
- pass
-
-
-class NonExistingTime(TimezoneError):
-
- message = "The datetime {} does not exist."
-
- def __init__(self, dt):
- message = self.message.format(dt)
-
- super(NonExistingTime, self).__init__(message)
-
-
-class AmbiguousTime(TimezoneError):
-
- message = "The datetime {} is ambiguous."
-
- def __init__(self, dt):
- message = self.message.format(dt)
-
- super(AmbiguousTime, self).__init__(message)
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from datetime import datetime
+
+
+class TimezoneError(ValueError):
+ pass
+
+
+class InvalidTimezone(TimezoneError):
+ pass
+
+
+class NonExistingTime(TimezoneError):
+ message = "The datetime {} does not exist."
+
+ def __init__(self, dt: datetime) -> None:
+ message = self.message.format(dt)
+
+ super().__init__(message)
+
+
+class AmbiguousTime(TimezoneError):
+ message = "The datetime {} is ambiguous."
+
+ def __init__(self, dt: datetime) -> None:
+ message = self.message.format(dt)
+
+ super().__init__(message)
diff --git a/pendulum/tz/local_timezone.py b/pendulum/tz/local_timezone.py
index 756105a..41cf81b 100644
--- a/pendulum/tz/local_timezone.py
+++ b/pendulum/tz/local_timezone.py
@@ -1,257 +1,260 @@
-import os
-import re
-import sys
-
-from contextlib import contextmanager
-from typing import Iterator
-from typing import Optional
-from typing import Union
-
-from .timezone import Timezone
-from .timezone import TimezoneFile
-from .zoneinfo.exceptions import InvalidTimezone
-
-
-try:
- import _winreg as winreg
-except ImportError:
- try:
- import winreg
- except ImportError:
- winreg = None
-
-
-_mock_local_timezone = None
-_local_timezone = None
-
-
-def get_local_timezone(): # type: () -> Timezone
- global _local_timezone
-
- if _mock_local_timezone is not None:
- return _mock_local_timezone
-
- if _local_timezone is None:
- tz = _get_system_timezone()
-
- _local_timezone = tz
-
- return _local_timezone
-
-
-def set_local_timezone(mock=None): # type: (Optional[Union[str, Timezone]]) -> None
- global _mock_local_timezone
-
- _mock_local_timezone = mock
-
-
-@contextmanager
-def test_local_timezone(mock): # type: (Timezone) -> Iterator[None]
- set_local_timezone(mock)
-
- yield
-
- set_local_timezone()
-
-
-def _get_system_timezone(): # type: () -> Timezone
- if sys.platform == "win32":
- return _get_windows_timezone()
- elif "darwin" in sys.platform:
- return _get_darwin_timezone()
-
- return _get_unix_timezone()
-
-
-def _get_windows_timezone(): # type: () -> Timezone
- from .data.windows import windows_timezones
-
- # Windows is special. It has unique time zone names (in several
- # meanings of the word) available, but unfortunately, they can be
- # translated to the language of the operating system, so we need to
- # do a backwards lookup, by going through all time zones and see which
- # one matches.
- handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
-
- tz_local_key_name = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
- localtz = winreg.OpenKey(handle, tz_local_key_name)
-
- timezone_info = {}
- size = winreg.QueryInfoKey(localtz)[1]
- for i in range(size):
- data = winreg.EnumValue(localtz, i)
- timezone_info[data[0]] = data[1]
-
- localtz.Close()
-
- if "TimeZoneKeyName" in timezone_info:
- # Windows 7 (and Vista?)
-
- # For some reason this returns a string with loads of NUL bytes at
- # least on some systems. I don't know if this is a bug somewhere, I
- # just work around it.
- tzkeyname = timezone_info["TimeZoneKeyName"].split("\x00", 1)[0]
- else:
- # Windows 2000 or XP
-
- # This is the localized name:
- tzwin = timezone_info["StandardName"]
-
- # Open the list of timezones to look up the real name:
- tz_key_name = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
- tzkey = winreg.OpenKey(handle, tz_key_name)
-
- # Now, match this value to Time Zone information
- tzkeyname = None
- for i in range(winreg.QueryInfoKey(tzkey)[0]):
- subkey = winreg.EnumKey(tzkey, i)
- sub = winreg.OpenKey(tzkey, subkey)
-
- info = {}
- size = winreg.QueryInfoKey(sub)[1]
- for i in range(size):
- data = winreg.EnumValue(sub, i)
- info[data[0]] = data[1]
-
- sub.Close()
- try:
- if info["Std"] == tzwin:
- tzkeyname = subkey
- break
- except KeyError:
- # This timezone didn't have proper configuration.
- # Ignore it.
- pass
-
- tzkey.Close()
- handle.Close()
-
- if tzkeyname is None:
- raise LookupError("Can not find Windows timezone configuration")
-
- timezone = windows_timezones.get(tzkeyname)
- if timezone is None:
- # Nope, that didn't work. Try adding "Standard Time",
- # it seems to work a lot of times:
- timezone = windows_timezones.get(tzkeyname + " Standard Time")
-
- # Return what we have.
- if timezone is None:
- raise LookupError("Unable to find timezone " + tzkeyname)
-
- return Timezone(timezone)
-
-
-def _get_darwin_timezone(): # type: () -> Timezone
- # link will be something like /usr/share/zoneinfo/America/Los_Angeles.
- link = os.readlink("/etc/localtime")
- tzname = link[link.rfind("zoneinfo/") + 9 :]
-
- return Timezone(tzname)
-
-
-def _get_unix_timezone(_root="/"): # type: (str) -> Timezone
- tzenv = os.environ.get("TZ")
- if tzenv:
- try:
- return _tz_from_env(tzenv)
- except ValueError:
- pass
-
- # Now look for distribution specific configuration files
- # that contain the timezone name.
- tzpath = os.path.join(_root, "etc/timezone")
- if os.path.exists(tzpath):
- with open(tzpath, "rb") as tzfile:
- data = tzfile.read()
-
- # Issue #3 was that /etc/timezone was a zoneinfo file.
- # That's a misconfiguration, but we need to handle it gracefully:
- if data[:5] != "TZif2":
- etctz = data.strip().decode()
- # Get rid of host definitions and comments:
- if " " in etctz:
- etctz, dummy = etctz.split(" ", 1)
- if "#" in etctz:
- etctz, dummy = etctz.split("#", 1)
-
- return Timezone(etctz.replace(" ", "_"))
-
- # CentOS has a ZONE setting in /etc/sysconfig/clock,
- # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and
- # Gentoo has a TIMEZONE setting in /etc/conf.d/clock
- # We look through these files for a timezone:
- zone_re = re.compile(r'\s*ZONE\s*=\s*"')
- timezone_re = re.compile(r'\s*TIMEZONE\s*=\s*"')
- end_re = re.compile('"')
-
- for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"):
- tzpath = os.path.join(_root, filename)
- if not os.path.exists(tzpath):
- continue
-
- with open(tzpath, "rt") as tzfile:
- data = tzfile.readlines()
-
- for line in data:
- # Look for the ZONE= setting.
- match = zone_re.match(line)
- if match is None:
- # No ZONE= setting. Look for the TIMEZONE= setting.
- match = timezone_re.match(line)
-
- if match is not None:
- # Some setting existed
- line = line[match.end() :]
- etctz = line[: end_re.search(line).start()]
-
- parts = list(reversed(etctz.replace(" ", "_").split(os.path.sep)))
- tzpath = []
- while parts:
- tzpath.insert(0, parts.pop(0))
-
- try:
- return Timezone(os.path.join(*tzpath))
- except InvalidTimezone:
- pass
-
- # systemd distributions use symlinks that include the zone name,
- # see manpage of localtime(5) and timedatectl(1)
- tzpath = os.path.join(_root, "etc", "localtime")
- if os.path.exists(tzpath) and os.path.islink(tzpath):
- parts = list(
- reversed(os.path.realpath(tzpath).replace(" ", "_").split(os.path.sep))
- )
- tzpath = []
- while parts:
- tzpath.insert(0, parts.pop(0))
- try:
- return Timezone(os.path.join(*tzpath))
- except InvalidTimezone:
- pass
-
- # No explicit setting existed. Use localtime
- for filename in ("etc/localtime", "usr/local/etc/localtime"):
- tzpath = os.path.join(_root, filename)
-
- if not os.path.exists(tzpath):
- continue
-
- return TimezoneFile(tzpath)
-
- raise RuntimeError("Unable to find any timezone configuration")
-
-
-def _tz_from_env(tzenv): # type: (str) -> Timezone
- if tzenv[0] == ":":
- tzenv = tzenv[1:]
-
- # TZ specifies a file
- if os.path.exists(tzenv):
- return TimezoneFile(tzenv)
-
- # TZ specifies a zoneinfo zone.
- try:
- return Timezone(tzenv)
- except ValueError:
- raise
+from __future__ import annotations
+
+import contextlib
+import os
+import re
+import sys
+
+from contextlib import contextmanager
+from typing import Iterator
+from typing import cast
+
+from pendulum.tz.exceptions import InvalidTimezone
+from pendulum.tz.timezone import FixedTimezone
+from pendulum.tz.timezone import Timezone
+
+if sys.platform == "win32":
+ try:
+ import _winreg as winreg
+ except (ImportError, AttributeError):
+ import winreg
+
+_mock_local_timezone = None
+_local_timezone = None
+
+
+def get_local_timezone() -> Timezone | FixedTimezone:
+ global _local_timezone
+
+ if _mock_local_timezone is not None:
+ return _mock_local_timezone
+
+ if _local_timezone is None:
+ tz = _get_system_timezone()
+
+ _local_timezone = tz
+
+ return _local_timezone
+
+
+def set_local_timezone(mock: str | Timezone | None = None) -> None:
+ global _mock_local_timezone
+
+ _mock_local_timezone = mock
+
+
+@contextmanager
+def test_local_timezone(mock: Timezone) -> Iterator[None]:
+ set_local_timezone(mock)
+
+ yield
+
+ set_local_timezone()
+
+
+def _get_system_timezone() -> Timezone:
+ if sys.platform == "win32":
+ return _get_windows_timezone()
+ elif "darwin" in sys.platform:
+ return _get_darwin_timezone()
+
+ return _get_unix_timezone()
+
+
+if sys.platform == "win32":
+
+ def _get_windows_timezone() -> Timezone:
+ from pendulum.tz.data.windows import windows_timezones
+
+ # Windows is special. It has unique time zone names (in several
+ # meanings of the word) available, but unfortunately, they can be
+ # translated to the language of the operating system, so we need to
+ # do a backwards lookup, by going through all time zones and see which
+ # one matches.
+ handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
+
+ tz_local_key_name = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
+ localtz = winreg.OpenKey(handle, tz_local_key_name)
+
+ timezone_info = {}
+ size = winreg.QueryInfoKey(localtz)[1]
+ for i in range(size):
+ data = winreg.EnumValue(localtz, i)
+ timezone_info[data[0]] = data[1]
+
+ localtz.Close()
+
+ if "TimeZoneKeyName" in timezone_info:
+ # Windows 7 (and Vista?)
+
+ # For some reason this returns a string with loads of NUL bytes at
+ # least on some systems. I don't know if this is a bug somewhere, I
+ # just work around it.
+ tzkeyname = timezone_info["TimeZoneKeyName"].split("\x00", 1)[0]
+ else:
+ # Windows 2000 or XP
+
+ # This is the localized name:
+ tzwin = timezone_info["StandardName"]
+
+ # Open the list of timezones to look up the real name:
+ tz_key_name = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
+ tzkey = winreg.OpenKey(handle, tz_key_name)
+
+ # Now, match this value to Time Zone information
+ tzkeyname = None
+ for i in range(winreg.QueryInfoKey(tzkey)[0]):
+ subkey = winreg.EnumKey(tzkey, i)
+ sub = winreg.OpenKey(tzkey, subkey)
+
+ info = {}
+ size = winreg.QueryInfoKey(sub)[1]
+ for i in range(size):
+ data = winreg.EnumValue(sub, i)
+ info[data[0]] = data[1]
+
+ sub.Close()
+ with contextlib.suppress(KeyError):
+ # This timezone didn't have proper configuration.
+ # Ignore it.
+ if info["Std"] == tzwin:
+ tzkeyname = subkey
+ break
+
+ tzkey.Close()
+ handle.Close()
+
+ if tzkeyname is None:
+ raise LookupError("Can not find Windows timezone configuration")
+
+ timezone = windows_timezones.get(tzkeyname)
+ if timezone is None:
+ # Nope, that didn't work. Try adding "Standard Time",
+ # it seems to work a lot of times:
+ timezone = windows_timezones.get(tzkeyname + " Standard Time")
+
+ # Return what we have.
+ if timezone is None:
+ raise LookupError("Unable to find timezone " + tzkeyname)
+
+ return Timezone(timezone)
+
+else:
+
+ def _get_windows_timezone() -> Timezone:
+ ...
+
+
+def _get_darwin_timezone() -> Timezone:
+ # link will be something like /usr/share/zoneinfo/America/Los_Angeles.
+ link = os.readlink("/etc/localtime")
+ tzname = link[link.rfind("zoneinfo/") + 9 :]
+
+ return Timezone(tzname)
+
+
+def _get_unix_timezone(_root: str = "/") -> Timezone:
+ tzenv = os.environ.get("TZ")
+ if tzenv:
+ with contextlib.suppress(ValueError):
+ return _tz_from_env(tzenv)
+
+ # Now look for distribution specific configuration files
+ # that contain the timezone name.
+ tzpath = os.path.join(_root, "etc/timezone")
+ if os.path.isfile(tzpath):
+ with open(tzpath, "rb") as tzfile:
+ tzfile_data = tzfile.read()
+
+ # Issue #3 was that /etc/timezone was a zoneinfo file.
+ # That's a misconfiguration, but we need to handle it gracefully:
+ if tzfile_data[:5] != b"TZif2":
+ etctz = tzfile_data.strip().decode()
+ # Get rid of host definitions and comments:
+ if " " in etctz:
+ etctz, dummy = etctz.split(" ", 1)
+ if "#" in etctz:
+ etctz, dummy = etctz.split("#", 1)
+
+ return Timezone(etctz.replace(" ", "_"))
+
+ # CentOS has a ZONE setting in /etc/sysconfig/clock,
+ # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and
+ # Gentoo has a TIMEZONE setting in /etc/conf.d/clock
+ # We look through these files for a timezone:
+ zone_re = re.compile(r'\s*ZONE\s*=\s*"')
+ timezone_re = re.compile(r'\s*TIMEZONE\s*=\s*"')
+ end_re = re.compile('"')
+
+ for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"):
+ tzpath = os.path.join(_root, filename)
+ if not os.path.isfile(tzpath):
+ continue
+
+ with open(tzpath) as tzfile:
+ data = tzfile.readlines()
+
+ for line in data:
+ # Look for the ZONE= setting.
+ match = zone_re.match(line)
+ if match is None:
+ # No ZONE= setting. Look for the TIMEZONE= setting.
+ match = timezone_re.match(line)
+
+ if match is not None:
+ # Some setting existed
+ line = line[match.end() :]
+ etctz = line[
+ : cast(
+ re.Match, end_re.search(line) # type: ignore[type-arg]
+ ).start()
+ ]
+
+ parts = list(reversed(etctz.replace(" ", "_").split(os.path.sep)))
+ tzpath_parts: list[str] = []
+ while parts:
+ tzpath_parts.insert(0, parts.pop(0))
+
+ with contextlib.suppress(InvalidTimezone):
+ return Timezone(os.path.join(*tzpath_parts))
+
+ # systemd distributions use symlinks that include the zone name,
+ # see manpage of localtime(5) and timedatectl(1)
+ tzpath = os.path.join(_root, "etc", "localtime")
+ if os.path.isfile(tzpath) and os.path.islink(tzpath):
+ parts = list(
+ reversed(os.path.realpath(tzpath).replace(" ", "_").split(os.path.sep))
+ )
+ tzpath_parts: list[str] = [] # type: ignore[no-redef]
+ while parts:
+ tzpath_parts.insert(0, parts.pop(0))
+ with contextlib.suppress(InvalidTimezone):
+ return Timezone(os.path.join(*tzpath_parts))
+
+ # No explicit setting existed. Use localtime
+ for filename in ("etc/localtime", "usr/local/etc/localtime"):
+ tzpath = os.path.join(_root, filename)
+
+ if not os.path.isfile(tzpath):
+ continue
+
+ with open(tzpath, "rb") as f:
+ return cast(Timezone, Timezone.from_file(f))
+
+ raise RuntimeError("Unable to find any timezone configuration")
+
+
+def _tz_from_env(tzenv: str) -> Timezone:
+ if tzenv[0] == ":":
+ tzenv = tzenv[1:]
+
+ # TZ specifies a file
+ if os.path.isfile(tzenv):
+ with open(tzenv, "rb") as f:
+ return cast(Timezone, Timezone.from_file(f))
+
+ # TZ specifies a zoneinfo zone.
+ try:
+ return Timezone(tzenv)
+ except ValueError:
+ raise
diff --git a/pendulum/tz/timezone.py b/pendulum/tz/timezone.py
index bc94d56..f689004 100644
--- a/pendulum/tz/timezone.py
+++ b/pendulum/tz/timezone.py
@@ -1,377 +1,217 @@
-from datetime import datetime
-from datetime import timedelta
-from datetime import tzinfo
-from typing import Optional
-from typing import TypeVar
-from typing import overload
-
-import pendulum
-
-from pendulum.helpers import local_time
-from pendulum.helpers import timestamp
-from pendulum.utils._compat import _HAS_FOLD
-
-from .exceptions import AmbiguousTime
-from .exceptions import NonExistingTime
-from .zoneinfo import read
-from .zoneinfo import read_file
-from .zoneinfo.transition import Transition
-
-
-POST_TRANSITION = "post"
-PRE_TRANSITION = "pre"
-TRANSITION_ERROR = "error"
-
-_datetime = datetime
-_D = TypeVar("_D", bound=datetime)
-
-
-class Timezone(tzinfo):
- """
- Represents a named timezone.
-
- The accepted names are those provided by the IANA time zone database.
-
- >>> from pendulum.tz.timezone import Timezone
- >>> tz = Timezone('Europe/Paris')
- """
-
- def __init__(self, name, extended=True): # type: (str, bool) -> None
- tz = read(name, extend=extended)
-
- self._name = name
- self._transitions = tz.transitions
- self._hint = {True: None, False: None}
-
- @property
- def name(self): # type: () -> str
- return self._name
-
- def convert(self, dt, dst_rule=None): # type: (_D, Optional[str]) -> _D
- """
- Converts a datetime in the current timezone.
-
- If the datetime is naive, it will be normalized.
-
- >>> from datetime import datetime
- >>> from pendulum import timezone
- >>> paris = timezone('Europe/Paris')
- >>> dt = datetime(2013, 3, 31, 2, 30, fold=1)
- >>> in_paris = paris.convert(dt)
- >>> in_paris.isoformat()
- '2013-03-31T03:30:00+02:00'
-
- If the datetime is aware, it will be properly converted.
-
- >>> new_york = timezone('America/New_York')
- >>> in_new_york = new_york.convert(in_paris)
- >>> in_new_york.isoformat()
- '2013-03-30T21:30:00-04:00'
- """
- if dt.tzinfo is None:
- return self._normalize(dt, dst_rule=dst_rule)
-
- return self._convert(dt)
-
- def datetime(
- self, year, month, day, hour=0, minute=0, second=0, microsecond=0
- ): # type: (int, int, int, int, int, int, int) -> _datetime
- """
- Return a normalized datetime for the current timezone.
- """
- if _HAS_FOLD:
- return self.convert(
- datetime(year, month, day, hour, minute, second, microsecond, fold=1)
- )
-
- return self.convert(
- datetime(year, month, day, hour, minute, second, microsecond),
- dst_rule=POST_TRANSITION,
- )
-
- def _normalize(self, dt, dst_rule=None): # type: (_D, Optional[str]) -> _D
- sec = timestamp(dt)
- fold = 0
- transition = self._lookup_transition(sec)
-
- if not _HAS_FOLD and dst_rule is None:
- dst_rule = POST_TRANSITION
-
- if dst_rule is None:
- dst_rule = PRE_TRANSITION
- if dt.fold == 1:
- dst_rule = POST_TRANSITION
-
- if sec < transition.local:
- if transition.is_ambiguous(sec):
- # Ambiguous time
- if dst_rule == TRANSITION_ERROR:
- raise AmbiguousTime(dt)
-
- # We set the fold attribute for later
- if dst_rule == POST_TRANSITION:
- fold = 1
- elif transition.previous is not None:
- transition = transition.previous
-
- if transition:
- if transition.is_ambiguous(sec):
- # Ambiguous time
- if dst_rule == TRANSITION_ERROR:
- raise AmbiguousTime(dt)
-
- # We set the fold attribute for later
- if dst_rule == POST_TRANSITION:
- fold = 1
- elif transition.is_missing(sec):
- # Skipped time
- if dst_rule == TRANSITION_ERROR:
- raise NonExistingTime(dt)
-
- # We adjust accordingly
- if dst_rule == POST_TRANSITION:
- sec += transition.fix
- fold = 1
- else:
- sec -= transition.fix
-
- kwargs = {"tzinfo": self}
- if _HAS_FOLD or isinstance(dt, pendulum.DateTime):
- kwargs["fold"] = fold
-
- return dt.__class__(*local_time(sec, 0, dt.microsecond), **kwargs)
-
- def _convert(self, dt): # type: (_D) -> _D
- if dt.tzinfo is self:
- return self._normalize(dt, dst_rule=POST_TRANSITION)
-
- if not isinstance(dt.tzinfo, Timezone):
- return dt.astimezone(self)
-
- stamp = timestamp(dt)
-
- if isinstance(dt.tzinfo, FixedTimezone):
- offset = dt.tzinfo.offset
- else:
- transition = dt.tzinfo._lookup_transition(stamp)
- offset = transition.ttype.offset
-
- if stamp < transition.local and transition.previous is not None:
- if (
- transition.previous.is_ambiguous(stamp)
- and getattr(dt, "fold", 1) == 0
- ):
- pass
- else:
- offset = transition.previous.ttype.offset
-
- stamp -= offset
-
- transition = self._lookup_transition(stamp, is_utc=True)
- if stamp < transition.at and transition.previous is not None:
- transition = transition.previous
-
- offset = transition.ttype.offset
- stamp += offset
- fold = int(not transition.ttype.is_dst())
-
- kwargs = {"tzinfo": self}
-
- if _HAS_FOLD or isinstance(dt, pendulum.DateTime):
- kwargs["fold"] = fold
-
- return dt.__class__(*local_time(stamp, 0, dt.microsecond), **kwargs)
-
- def _lookup_transition(
- self, stamp, is_utc=False
- ): # type: (int, bool) -> Transition
- lo, hi = 0, len(self._transitions)
- hint = self._hint[is_utc]
- if hint:
- if stamp == hint[0]:
- return self._transitions[hint[1]]
- elif stamp < hint[0]:
- hi = hint[1]
- else:
- lo = hint[1]
-
- if not is_utc:
- while lo < hi:
- mid = (lo + hi) // 2
- if stamp < self._transitions[mid].to:
- hi = mid
- else:
- lo = mid + 1
- else:
- while lo < hi:
- mid = (lo + hi) // 2
- if stamp < self._transitions[mid].at:
- hi = mid
- else:
- lo = mid + 1
-
- if lo >= len(self._transitions):
- # Beyond last transition
- lo = len(self._transitions) - 1
-
- self._hint[is_utc] = (stamp, lo)
-
- return self._transitions[lo]
-
- @overload
- def utcoffset(self, dt): # type: (None) -> None
- pass
-
- @overload
- def utcoffset(self, dt): # type: (_datetime) -> timedelta
- pass
-
- def utcoffset(self, dt):
- if dt is None:
- return
-
- transition = self._get_transition(dt)
-
- return transition.utcoffset()
-
- def dst(
- self, dt # type: Optional[_datetime]
- ): # type: (...) -> Optional[timedelta]
- if dt is None:
- return
-
- transition = self._get_transition(dt)
-
- if not transition.ttype.is_dst():
- return timedelta()
-
- return timedelta(seconds=transition.fix)
-
- def tzname(self, dt): # type: (Optional[_datetime]) -> Optional[str]
- if dt is None:
- return
-
- transition = self._get_transition(dt)
-
- return transition.ttype.abbreviation
-
- def _get_transition(self, dt): # type: (_datetime) -> Transition
- if dt.tzinfo is not None and dt.tzinfo is not self:
- dt = dt - dt.utcoffset()
-
- stamp = timestamp(dt)
-
- transition = self._lookup_transition(stamp, is_utc=True)
- else:
- stamp = timestamp(dt)
-
- transition = self._lookup_transition(stamp)
-
- if stamp < transition.local and transition.previous is not None:
- fold = getattr(dt, "fold", 1)
- if transition.is_ambiguous(stamp):
- if fold == 0:
- transition = transition.previous
- elif transition.previous.is_ambiguous(stamp) and fold == 0:
- pass
- else:
- transition = transition.previous
-
- return transition
-
- def fromutc(self, dt): # type: (_D) -> _D
- stamp = timestamp(dt)
-
- transition = self._lookup_transition(stamp, is_utc=True)
- if stamp < transition.at and transition.previous is not None:
- transition = transition.previous
-
- stamp += transition.ttype.offset
-
- return dt.__class__(*local_time(stamp, 0, dt.microsecond), tzinfo=self)
-
- def __repr__(self): # type: () -> str
- return "Timezone('{}')".format(self._name)
-
- def __getinitargs__(self): # type: () -> tuple
- return (self._name,)
-
-
-class FixedTimezone(Timezone):
- def __init__(self, offset, name=None):
- sign = "-" if offset < 0 else "+"
-
- minutes = offset / 60
- hour, minute = divmod(abs(int(minutes)), 60)
-
- if not name:
- name = "{0}{1:02d}:{2:02d}".format(sign, hour, minute)
-
- self._name = name
- self._offset = offset
- self._utcoffset = timedelta(seconds=offset)
-
- @property
- def offset(self): # type: () -> int
- return self._offset
-
- def _normalize(self, dt, dst_rule=None): # type: (_D, Optional[str]) -> _D
- if _HAS_FOLD:
- dt = dt.__class__(
- dt.year,
- dt.month,
- dt.day,
- dt.hour,
- dt.minute,
- dt.second,
- dt.microsecond,
- tzinfo=self,
- fold=0,
- )
- else:
- dt = dt.__class__(
- dt.year,
- dt.month,
- dt.day,
- dt.hour,
- dt.minute,
- dt.second,
- dt.microsecond,
- tzinfo=self,
- )
-
- return dt
-
- def _convert(self, dt): # type: (_D) -> _D
- if dt.tzinfo is not self:
- return dt.astimezone(self)
-
- return dt
-
- def utcoffset(self, dt): # type: (Optional[_datetime]) -> timedelta
- return self._utcoffset
-
- def dst(self, dt): # type: (Optional[_datetime]) -> timedelta
- return timedelta()
-
- def fromutc(self, dt): # type: (_D) -> _D
- # Use the stdlib datetime's add method to avoid infinite recursion
- return (datetime.__add__(dt, self._utcoffset)).replace(tzinfo=self)
-
- def tzname(self, dt): # type: (Optional[_datetime]) -> Optional[str]
- return self._name
-
- def __getinitargs__(self): # type: () -> tuple
- return self._offset, self._name
-
-
-class TimezoneFile(Timezone):
- def __init__(self, path):
- tz = read_file(path)
-
- self._name = ""
- self._transitions = tz.transitions
- self._hint = {True: None, False: None}
-
-
-UTC = FixedTimezone(0, "UTC")
+from __future__ import annotations
+
+import datetime as datetime_
+
+from abc import ABC
+from abc import abstractmethod
+from typing import cast
+
+from pendulum.tz.exceptions import AmbiguousTime
+from pendulum.tz.exceptions import InvalidTimezone
+from pendulum.tz.exceptions import NonExistingTime
+from pendulum.utils._compat import zoneinfo
+
+POST_TRANSITION = "post"
+PRE_TRANSITION = "pre"
+TRANSITION_ERROR = "error"
+
+
+class PendulumTimezone(ABC):
+ @property
+ @abstractmethod
+ def name(self) -> str:
+ raise NotImplementedError
+
+ @abstractmethod
+ def convert(
+ self, dt: datetime_.datetime, raise_on_unknown_times: bool = False
+ ) -> datetime_.datetime:
+ raise NotImplementedError
+
+ @abstractmethod
+ def datetime(
+ self,
+ year: int,
+ month: int,
+ day: int,
+ hour: int = 0,
+ minute: int = 0,
+ second: int = 0,
+ microsecond: int = 0,
+ ) -> datetime_.datetime:
+ raise NotImplementedError
+
+
+class Timezone(zoneinfo.ZoneInfo, PendulumTimezone): # type: ignore[misc]
+ """
+ Represents a named timezone.
+
+ The accepted names are those provided by the IANA time zone database.
+
+ >>> from pendulum.tz.timezone import Timezone
+ >>> tz = Timezone('Europe/Paris')
+ """
+
+ def __new__(cls, key: str) -> Timezone:
+ try:
+ return cast(Timezone, super().__new__(cls, key))
+ except zoneinfo.ZoneInfoNotFoundError:
+ raise InvalidTimezone(key)
+
+ @property
+ def name(self) -> str:
+ return cast(str, self.key)
+
+ def convert(
+ self, dt: datetime_.datetime, raise_on_unknown_times: bool = False
+ ) -> datetime_.datetime:
+ """
+ Converts a datetime in the current timezone.
+
+ If the datetime is naive, it will be normalized.
+
+ >>> from datetime import datetime
+ >>> from pendulum import timezone
+ >>> paris = timezone('Europe/Paris')
+ >>> dt = datetime(2013, 3, 31, 2, 30, fold=1)
+ >>> in_paris = paris.convert(dt)
+ >>> in_paris.isoformat()
+ '2013-03-31T03:30:00+02:00'
+
+ If the datetime is aware, it will be properly converted.
+
+ >>> new_york = timezone('America/New_York')
+ >>> in_new_york = new_york.convert(in_paris)
+ >>> in_new_york.isoformat()
+ '2013-03-30T21:30:00-04:00'
+ """
+ if dt.tzinfo is None:
+ offset_before = (
+ self.utcoffset(dt.replace(fold=0)) if dt.fold else self.utcoffset(dt)
+ )
+ offset_after = (
+ self.utcoffset(dt) if dt.fold else self.utcoffset(dt.replace(fold=1))
+ )
+
+ if offset_after > offset_before:
+ # Skipped time
+ if raise_on_unknown_times:
+ raise NonExistingTime(dt)
+
+ dt += (
+ (offset_after - offset_before)
+ if dt.fold
+ else (offset_before - offset_after)
+ )
+ elif offset_before > offset_after and raise_on_unknown_times:
+ # Repeated time
+ raise AmbiguousTime(dt)
+
+ return dt.replace(tzinfo=self)
+
+ return dt.astimezone(self)
+
+ def datetime(
+ self,
+ year: int,
+ month: int,
+ day: int,
+ hour: int = 0,
+ minute: int = 0,
+ second: int = 0,
+ microsecond: int = 0,
+ ) -> datetime_.datetime:
+ """
+ Return a normalized datetime for the current timezone.
+ """
+ return self.convert(
+ datetime_.datetime(
+ year, month, day, hour, minute, second, microsecond, fold=1
+ )
+ )
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}('{self.name}')"
+
+
+class FixedTimezone(datetime_.tzinfo, PendulumTimezone):
+ def __init__(self, offset: int, name: str | None = None) -> None:
+ sign = "-" if offset < 0 else "+"
+
+ minutes = offset / 60
+ hour, minute = divmod(abs(int(minutes)), 60)
+
+ if not name:
+ name = f"{sign}{hour:02d}:{minute:02d}"
+
+ self._name = name
+ self._offset = offset
+ self._utcoffset = datetime_.timedelta(seconds=offset)
+
+ @property
+ def name(self) -> str:
+ return self._name
+
+ def convert(
+ self, dt: datetime_.datetime, raise_on_unknown_times: bool = False
+ ) -> datetime_.datetime:
+ if dt.tzinfo is None:
+ return dt.__class__(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ tzinfo=self,
+ fold=0,
+ )
+
+ return dt.astimezone(self)
+
+ def datetime(
+ self,
+ year: int,
+ month: int,
+ day: int,
+ hour: int = 0,
+ minute: int = 0,
+ second: int = 0,
+ microsecond: int = 0,
+ ) -> datetime_.datetime:
+ return self.convert(
+ datetime_.datetime(
+ year, month, day, hour, minute, second, microsecond, fold=1
+ )
+ )
+
+ @property
+ def offset(self) -> int:
+ return self._offset
+
+ def utcoffset(self, dt: datetime_.datetime | None) -> datetime_.timedelta:
+ return self._utcoffset
+
+ def dst(self, dt: datetime_.datetime | None) -> datetime_.timedelta:
+ return datetime_.timedelta()
+
+ def fromutc(self, dt: datetime_.datetime) -> datetime_.datetime:
+ # Use the stdlib datetime's add method to avoid infinite recursion
+ return (datetime_.datetime.__add__(dt, self._utcoffset)).replace(tzinfo=self)
+
+ def tzname(self, dt: datetime_.datetime | None) -> str | None:
+ return self._name
+
+ def __getinitargs__(self) -> tuple[int, str]:
+ return self._offset, self._name
+
+ def __repr__(self) -> str:
+ name = ""
+ if self._name:
+ name = f', name="{self._name}"'
+
+ return f"{self.__class__.__name__}({self._offset}{name})"
+
+
+UTC = Timezone("UTC")
diff --git a/pendulum/tz/zoneinfo/__init__.py b/pendulum/tz/zoneinfo/__init__.py
deleted file mode 100644
index 890351a..0000000
--- a/pendulum/tz/zoneinfo/__init__.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from .reader import Reader
-from .timezone import Timezone
-
-
-def read(name, extend=True): # type: (str, bool) -> Timezone
- """
- Read the zoneinfo structure for a given timezone name.
- """
- return Reader(extend=extend).read_for(name)
-
-
-def read_file(path, extend=True): # type: (str, bool) -> Timezone
- """
- Read the zoneinfo structure for a given path.
- """
- return Reader(extend=extend).read(path)
diff --git a/pendulum/tz/zoneinfo/exceptions.py b/pendulum/tz/zoneinfo/exceptions.py
deleted file mode 100644
index 6e29ae2..0000000
--- a/pendulum/tz/zoneinfo/exceptions.py
+++ /dev/null
@@ -1,18 +0,0 @@
-class ZoneinfoError(Exception):
-
- pass
-
-
-class InvalidZoneinfoFile(ZoneinfoError):
-
- pass
-
-
-class InvalidTimezone(ZoneinfoError):
- def __init__(self, name):
- super(InvalidTimezone, self).__init__('Invalid timezone "{}"'.format(name))
-
-
-class InvalidPosixSpec(ZoneinfoError):
- def __init__(self, spec):
- super(InvalidPosixSpec, self).__init__("Invalid POSIX spec: {}".format(spec))
diff --git a/pendulum/tz/zoneinfo/posix_timezone.py b/pendulum/tz/zoneinfo/posix_timezone.py
deleted file mode 100644
index a6a7c72..0000000
--- a/pendulum/tz/zoneinfo/posix_timezone.py
+++ /dev/null
@@ -1,270 +0,0 @@
-"""
-Parsing of a POSIX zone spec as described in the TZ part of section 8.3 in
-http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html.
-"""
-import re
-
-from typing import Optional
-
-from pendulum.constants import MONTHS_OFFSETS
-from pendulum.constants import SECS_PER_DAY
-
-from .exceptions import InvalidPosixSpec
-
-
-_spec = re.compile(
- "^"
- r"(?P<std_abbr><.*?>|[^-+,\d]{3,})"
- r"(?P<std_offset>([+-])?(\d{1,2})(:\d{2}(:\d{2})?)?)"
- r"(?P<dst_info>"
- r" (?P<dst_abbr><.*?>|[^-+,\d]{3,})"
- r" (?P<dst_offset>([+-])?(\d{1,2})(:\d{2}(:\d{2})?)?)?"
- r")?"
- r"(?:,(?P<rules>"
- r" (?P<dst_start>"
- r" (?:J\d+|\d+|M\d{1,2}.\d.[0-6])"
- r" (?:/(?P<dst_start_offset>([+-])?(\d+)(:\d{2}(:\d{2})?)?))?"
- " )"
- " ,"
- r" (?P<dst_end>"
- r" (?:J\d+|\d+|M\d{1,2}.\d.[0-6])"
- r" (?:/(?P<dst_end_offset>([+-])?(\d+)(:\d{2}(:\d{2})?)?))?"
- " )"
- "))?"
- "$",
- re.VERBOSE,
-)
-
-
-def posix_spec(spec): # type: (str) -> PosixTimezone
- try:
- return _posix_spec(spec)
- except ValueError:
- raise InvalidPosixSpec(spec)
-
-
-def _posix_spec(spec): # type: (str) -> PosixTimezone
- m = _spec.match(spec)
- if not m:
- raise ValueError("Invalid posix spec")
-
- std_abbr = _parse_abbr(m.group("std_abbr"))
- std_offset = _parse_offset(m.group("std_offset"))
-
- dst_abbr = None
- dst_offset = None
- if m.group("dst_info"):
- dst_abbr = _parse_abbr(m.group("dst_abbr"))
- if m.group("dst_offset"):
- dst_offset = _parse_offset(m.group("dst_offset"))
- else:
- dst_offset = std_offset + 3600
-
- dst_start = None
- dst_end = None
- if m.group("rules"):
- dst_start = _parse_rule(m.group("dst_start"))
- dst_end = _parse_rule(m.group("dst_end"))
-
- return PosixTimezone(std_abbr, std_offset, dst_abbr, dst_offset, dst_start, dst_end)
-
-
-def _parse_abbr(text): # type: (str) -> str
- return text.lstrip("<").rstrip(">")
-
-
-def _parse_offset(text, sign=-1): # type: (str, int) -> int
- if text.startswith(("+", "-")):
- if text.startswith("-"):
- sign *= -1
-
- text = text[1:]
-
- minutes = 0
- seconds = 0
-
- parts = text.split(":")
- hours = int(parts[0])
-
- if len(parts) > 1:
- minutes = int(parts[1])
-
- if len(parts) > 2:
- seconds = int(parts[2])
-
- return sign * ((((hours * 60) + minutes) * 60) + seconds)
-
-
-def _parse_rule(rule): # type: (str) -> PosixTransition
- klass = NPosixTransition
- args = ()
-
- if rule.startswith("M"):
- rule = rule[1:]
- parts = rule.split(".")
- month = int(parts[0])
- week = int(parts[1])
- day = int(parts[2].split("/")[0])
-
- args += (month, week, day)
- klass = MPosixTransition
- elif rule.startswith("J"):
- rule = rule[1:]
- args += (int(rule.split("/")[0]),)
- klass = JPosixTransition
- else:
- args += (int(rule.split("/")[0]),)
-
- # Checking offset
- parts = rule.split("/")
- if len(parts) > 1:
- offset = _parse_offset(parts[-1], sign=1)
- else:
- offset = 7200
-
- args += (offset,)
-
- return klass(*args)
-
-
-class PosixTransition(object):
- def __init__(self, offset): # type: (int) -> None
- self._offset = offset
-
- @property
- def offset(self): # type: () -> int
- return self._offset
-
- def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int
- raise NotImplementedError()
-
-
-class JPosixTransition(PosixTransition):
- def __init__(self, day, offset): # type: (int, int) -> None
- self._day = day
-
- super(JPosixTransition, self).__init__(offset)
-
- @property
- def day(self): # type: () -> int
- """
- day of non-leap year [1:365]
- """
- return self._day
-
- def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int
- days = self._day
- if not is_leap or days < MONTHS_OFFSETS[1][3]:
- days -= 1
-
- return (days * SECS_PER_DAY) + self._offset
-
-
-class NPosixTransition(PosixTransition):
- def __init__(self, day, offset): # type: (int, int) -> None
- self._day = day
-
- super(NPosixTransition, self).__init__(offset)
-
- @property
- def day(self): # type: () -> int
- """
- day of year [0:365]
- """
- return self._day
-
- def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int
- days = self._day
-
- return (days * SECS_PER_DAY) + self._offset
-
-
-class MPosixTransition(PosixTransition):
- def __init__(self, month, week, weekday, offset):
- # type: (int, int, int, int) -> None
- self._month = month
- self._week = week
- self._weekday = weekday
-
- super(MPosixTransition, self).__init__(offset)
-
- @property
- def month(self): # type: () -> int
- """
- month of year [1:12]
- """
- return self._month
-
- @property
- def week(self): # type: () -> int
- """
- week of month [1:5] (5==last)
- """
- return self._week
-
- @property
- def weekday(self): # type: () -> int
- """
- 0==Sun, ..., 6=Sat
- """
- return self._weekday
-
- def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int
- last_week = self._week == 5
- days = MONTHS_OFFSETS[is_leap][self._month + int(last_week)]
- weekday = (jan1_weekday + days) % 7
- if last_week:
- days -= (weekday + 7 - 1 - self._weekday) % 7 + 1
- else:
- days += (self._weekday + 7 - weekday) % 7
- days += (self._week - 1) * 7
-
- return (days * SECS_PER_DAY) + self._offset
-
-
-class PosixTimezone:
- """
- The entirety of a POSIX-string specified time-zone rule.
-
- The standard abbreviation and offset are always given.
- """
-
- def __init__(
- self,
- std_abbr, # type: str
- std_offset, # type: int
- dst_abbr, # type: Optional[str]
- dst_offset, # type: Optional[int]
- dst_start=None, # type: Optional[PosixTransition]
- dst_end=None, # type: Optional[PosixTransition]
- ):
- self._std_abbr = std_abbr
- self._std_offset = std_offset
- self._dst_abbr = dst_abbr
- self._dst_offset = dst_offset
- self._dst_start = dst_start
- self._dst_end = dst_end
-
- @property
- def std_abbr(self): # type: () -> str
- return self._std_abbr
-
- @property
- def std_offset(self): # type: () -> int
- return self._std_offset
-
- @property
- def dst_abbr(self): # type: () -> Optional[str]
- return self._dst_abbr
-
- @property
- def dst_offset(self): # type: () -> Optional[int]
- return self._dst_offset
-
- @property
- def dst_start(self): # type: () -> Optional[PosixTransition]
- return self._dst_start
-
- @property
- def dst_end(self): # type: () -> Optional[PosixTransition]
- return self._dst_end
diff --git a/pendulum/tz/zoneinfo/reader.py b/pendulum/tz/zoneinfo/reader.py
deleted file mode 100644
index 31cb933..0000000
--- a/pendulum/tz/zoneinfo/reader.py
+++ /dev/null
@@ -1,224 +0,0 @@
-import os
-
-from collections import namedtuple
-from struct import unpack
-from typing import IO
-from typing import Any
-from typing import Dict
-from typing import List
-from typing import Optional
-from typing import Tuple
-
-import pytzdata
-
-from pytzdata.exceptions import TimezoneNotFound
-
-from pendulum.utils._compat import PY2
-
-from .exceptions import InvalidTimezone
-from .exceptions import InvalidZoneinfoFile
-from .posix_timezone import PosixTimezone
-from .posix_timezone import posix_spec
-from .timezone import Timezone
-from .transition import Transition
-from .transition_type import TransitionType
-
-
-_offset = namedtuple("offset", "utc_total_offset is_dst abbr_idx")
-
-header = namedtuple(
- "header",
- "version " "utclocals " "stdwalls " "leaps " "transitions " "types " "abbr_size",
-)
-
-
-class Reader:
- """
- Reads compiled zoneinfo TZif (\0, 2 or 3) files.
- """
-
- def __init__(self, extend=True): # type: (bool) -> None
- self._extend = extend
-
- def read_for(self, timezone): # type: (str) -> Timezone
- """
- Read the zoneinfo structure for a given timezone name.
-
- :param timezone: The timezone.
- """
- try:
- file_path = pytzdata.tz_path(timezone)
- except TimezoneNotFound:
- raise InvalidTimezone(timezone)
-
- return self.read(file_path)
-
- def read(self, file_path): # type: (str) -> Timezone
- """
- Read a zoneinfo structure from the given path.
-
- :param file_path: The path of a zoneinfo file.
- """
- if not os.path.exists(file_path):
- raise InvalidZoneinfoFile("The tzinfo file does not exist")
-
- with open(file_path, "rb") as fd:
- return self._parse(fd)
-
- def _check_read(self, fd, nbytes): # type: (...) -> bytes
- """
- Reads the given number of bytes from the given file
- and checks that the correct number of bytes could be read.
- """
- result = fd.read(nbytes)
-
- if (not result and nbytes > 0) or len(result) != nbytes:
- raise InvalidZoneinfoFile(
- "Expected {} bytes reading {}, "
- "but got {}".format(nbytes, fd.name, len(result) if result else 0)
- )
-
- if PY2:
- return bytearray(result)
-
- return result
-
- def _parse(self, fd): # type: (...) -> Timezone
- """
- Parse a zoneinfo file.
- """
- hdr = self._parse_header(fd)
-
- if hdr.version in (2, 3):
- # We're skipping the entire v1 file since
- # at least the same data will be found in TZFile 2.
- fd.seek(
- hdr.transitions * 5
- + hdr.types * 6
- + hdr.abbr_size
- + hdr.leaps * 4
- + hdr.stdwalls
- + hdr.utclocals,
- 1,
- )
-
- # Parse the second header
- hdr = self._parse_header(fd)
-
- if hdr.version != 2 and hdr.version != 3:
- raise InvalidZoneinfoFile(
- "Header versions mismatch for file {}".format(fd.name)
- )
-
- # Parse the v2 data
- trans = self._parse_trans_64(fd, hdr.transitions)
- type_idx = self._parse_type_idx(fd, hdr.transitions)
- types = self._parse_types(fd, hdr.types)
- abbrs = self._parse_abbrs(fd, hdr.abbr_size, types)
-
- fd.seek(hdr.leaps * 8 + hdr.stdwalls + hdr.utclocals, 1)
-
- trule = self._parse_posix_tz(fd)
- else:
- # TZFile v1
- trans = self._parse_trans_32(fd, hdr.transitions)
- type_idx = self._parse_type_idx(fd, hdr.transitions)
- types = self._parse_types(fd, hdr.types)
- abbrs = self._parse_abbrs(fd, hdr.abbr_size, types)
- trule = None
-
- types = [
- TransitionType(off, is_dst, abbrs[abbr]) for off, is_dst, abbr in types
- ]
-
- transitions = []
- previous = None
- for trans, idx in zip(trans, type_idx):
- transition = Transition(trans, types[idx], previous)
- transitions.append(transition)
-
- previous = transition
-
- if not transitions:
- transitions.append(Transition(0, types[0], None))
-
- return Timezone(transitions, posix_rule=trule, extended=self._extend)
-
- def _parse_header(self, fd): # type: (...) -> header
- buff = self._check_read(fd, 44)
-
- if buff[:4] != b"TZif":
- raise InvalidZoneinfoFile(
- 'The file "{}" has an invalid header.'.format(fd.name)
- )
-
- version = {0x00: 1, 0x32: 2, 0x33: 3}.get(buff[4])
-
- if version is None:
- raise InvalidZoneinfoFile(
- 'The file "{}" has an invalid version.'.format(fd.name)
- )
-
- hdr = header(version, *unpack(">6l", buff[20:44]))
-
- return hdr
-
- def _parse_trans_64(self, fd, n): # type: (IO[Any], int) -> List[int]
- trans = []
- for _ in range(n):
- buff = self._check_read(fd, 8)
- trans.append(unpack(">q", buff)[0])
-
- return trans
-
- def _parse_trans_32(self, fd, n): # type: (IO[Any], int) -> List[int]
- trans = []
- for _ in range(n):
- buff = self._check_read(fd, 4)
- trans.append(unpack(">i", buff)[0])
-
- return trans
-
- def _parse_type_idx(self, fd, n): # type: (IO[Any], int) -> List[int]
- buff = self._check_read(fd, n)
-
- return list(unpack("{}B".format(n), buff))
-
- def _parse_types(
- self, fd, n
- ): # type: (IO[Any], int) -> List[Tuple[Any, bool, int]]
- types = []
-
- for _ in range(n):
- buff = self._check_read(fd, 6)
- offset = unpack(">l", buff[:4])[0]
- is_dst = buff[4] == 1
- types.append((offset, is_dst, buff[5]))
-
- return types
-
- def _parse_abbrs(
- self, fd, n, types
- ): # type: (IO[Any], int, List[Tuple[Any, bool, int]]) -> Dict[int, str]
- abbrs = {}
- buff = self._check_read(fd, n)
-
- for offset, is_dst, idx in types:
- if idx not in abbrs:
- abbr = buff[idx : buff.find(b"\0", idx)].decode("utf-8")
- abbrs[idx] = abbr
-
- return abbrs
-
- def _parse_posix_tz(self, fd): # type: (...) -> Optional[PosixTimezone]
- s = fd.read().decode("utf-8")
-
- if not s.startswith("\n") or not s.endswith("\n"):
- raise InvalidZoneinfoFile('Invalid posix rule in file "{}"'.format(fd.name))
-
- s = s.strip()
-
- if not s:
- return
-
- return posix_spec(s)
diff --git a/pendulum/tz/zoneinfo/timezone.py b/pendulum/tz/zoneinfo/timezone.py
deleted file mode 100644
index 2147774..0000000
--- a/pendulum/tz/zoneinfo/timezone.py
+++ /dev/null
@@ -1,128 +0,0 @@
-from datetime import datetime
-from typing import List
-from typing import Optional
-
-from pendulum.constants import DAYS_PER_YEAR
-from pendulum.constants import SECS_PER_YEAR
-from pendulum.helpers import is_leap
-from pendulum.helpers import local_time
-from pendulum.helpers import timestamp
-from pendulum.helpers import week_day
-
-from .posix_timezone import PosixTimezone
-from .transition import Transition
-from .transition_type import TransitionType
-
-
-class Timezone:
- def __init__(
- self,
- transitions, # type: List[Transition]
- posix_rule=None, # type: Optional[PosixTimezone]
- extended=True, # type: bool
- ):
- self._posix_rule = posix_rule
- self._transitions = transitions
-
- if extended:
- self._extends()
-
- @property
- def transitions(self): # type: () -> List[Transition]
- return self._transitions
-
- @property
- def posix_rule(self):
- return self._posix_rule
-
- def _extends(self):
- if not self._posix_rule:
- return
-
- posix = self._posix_rule
-
- if not posix.dst_abbr:
- # std only
- # The future specification should match the last/default transition
- ttype = self._transitions[-1].ttype
- if not self._check_ttype(ttype, posix.std_offset, False, posix.std_abbr):
- raise ValueError("Posix spec does not match last transition")
-
- return
-
- if len(self._transitions) < 2:
- raise ValueError("Too few transitions for POSIX spec")
-
- # Extend the transitions for an additional 400 years
- # using the future specification
-
- # The future specification should match the last two transitions,
- # and those transitions should have different is_dst flags.
- tr0 = self._transitions[-1]
- tr1 = self._transitions[-2]
- tt0 = tr0.ttype
- tt1 = tr1.ttype
- if tt0.is_dst():
- dst = tt0
- std = tt1
- else:
- dst = tt1
- std = tt0
-
- self._check_ttype(dst, posix.dst_offset, True, posix.dst_abbr)
- self._check_ttype(std, posix.std_offset, False, posix.std_abbr)
-
- # Add the transitions to tr1 and back to tr0 for each extra year.
- last_year = local_time(tr0.local, 0, 0)[0]
- leap_year = is_leap(last_year)
- jan1 = datetime(last_year, 1, 1)
- jan1_time = timestamp(jan1)
- jan1_weekday = week_day(jan1.year, jan1.month, jan1.day) % 7
-
- if local_time(tr1.local, 0, 0)[0] != last_year:
- # Add a single extra transition to align to a calendar year.
- if tt0.is_dst():
- pt1 = posix.dst_end
- else:
- pt1 = posix.dst_start
-
- tr1_offset = pt1.trans_offset(leap_year, jan1_weekday)
- tr = Transition(jan1_time + tr1_offset - tt0.offset, tr1.ttype, tr0)
- tr0 = tr
- tr1 = tr0
- tt0 = tr0.ttype
- tt1 = tr1.ttype
-
- if tt0.is_dst():
- pt1 = posix.dst_end
- pt0 = posix.dst_start
- else:
- pt1 = posix.dst_start
- pt0 = posix.dst_end
-
- tr = tr0
- for year in range(last_year + 1, last_year + 401):
- jan1_time += SECS_PER_YEAR[leap_year]
- jan1_weekday = (jan1_weekday + DAYS_PER_YEAR[leap_year]) % 7
- leap_year = not leap_year and is_leap(year)
-
- tr1_offset = pt1.trans_offset(leap_year, jan1_weekday)
- tr = Transition(jan1_time + tr1_offset - tt0.offset, tt1, tr)
- self._transitions.append(tr)
-
- tr0_offset = pt0.trans_offset(leap_year, jan1_weekday)
- tr = Transition(jan1_time + tr0_offset - tt1.offset, tt0, tr)
- self._transitions.append(tr)
-
- def _check_ttype(
- self,
- ttype, # type: TransitionType
- offset, # type: int
- is_dst, # type: bool
- abbr, # type: str
- ): # type: (...) -> bool
- return (
- ttype.offset == offset
- and ttype.is_dst() == is_dst
- and ttype.abbreviation == abbr
- )
diff --git a/pendulum/tz/zoneinfo/transition.py b/pendulum/tz/zoneinfo/transition.py
deleted file mode 100644
index 7c6b2f7..0000000
--- a/pendulum/tz/zoneinfo/transition.py
+++ /dev/null
@@ -1,77 +0,0 @@
-from datetime import timedelta
-from typing import Optional
-
-from .transition_type import TransitionType
-
-
-class Transition:
- def __init__(
- self,
- at, # type: int
- ttype, # type: TransitionType
- previous, # type: Optional[Transition]
- ):
- self._at = at
-
- if previous:
- self._local = at + previous.ttype.offset
- else:
- self._local = at + ttype.offset
-
- self._ttype = ttype
- self._previous = previous
-
- if self.previous:
- self._fix = self._ttype.offset - self.previous.ttype.offset
- else:
- self._fix = 0
-
- self._to = self._local + self._fix
- self._to_utc = self._at + self._fix
- self._utcoffset = timedelta(seconds=ttype.offset)
-
- @property
- def at(self): # type: () -> int
- return self._at
-
- @property
- def local(self): # type: () -> int
- return self._local
-
- @property
- def to(self): # type: () -> int
- return self._to
-
- @property
- def to_utc(self): # type: () -> int
- return self._to
-
- @property
- def ttype(self): # type: () -> TransitionType
- return self._ttype
-
- @property
- def previous(self): # type: () -> Optional[Transition]
- return self._previous
-
- @property
- def fix(self): # type: () -> int
- return self._fix
-
- def is_ambiguous(self, stamp): # type: (int) -> bool
- return self._to <= stamp < self._local
-
- def is_missing(self, stamp): # type: (int) -> bool
- return self._local <= stamp < self._to
-
- def utcoffset(self): # type: () -> timedelta
- return self._utcoffset
-
- def __contains__(self, stamp): # type: (int) -> bool
- if self.previous is None:
- return stamp < self.local
-
- return self.previous.local <= stamp < self.local
-
- def __repr__(self): # type: () -> str
- return "Transition({} -> {}, {})".format(self._local, self._to, self._ttype)
diff --git a/pendulum/tz/zoneinfo/transition_type.py b/pendulum/tz/zoneinfo/transition_type.py
deleted file mode 100644
index dd0a634..0000000
--- a/pendulum/tz/zoneinfo/transition_type.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from datetime import timedelta
-
-from pendulum.utils._compat import PY2
-from pendulum.utils._compat import encode
-
-
-class TransitionType:
- def __init__(self, offset, is_dst, abbr):
- self._offset = offset
- self._is_dst = is_dst
- self._abbr = abbr
-
- self._utcoffset = timedelta(seconds=offset)
-
- @property
- def offset(self): # type: () -> int
- return self._offset
-
- @property
- def abbreviation(self): # type: () -> str
- if PY2:
- return encode(self._abbr)
-
- return self._abbr
-
- def is_dst(self): # type: () -> bool
- return self._is_dst
-
- def utcoffset(self): # type: () -> timedelta
- return self._utcoffset
-
- def __repr__(self): # type: () -> str
- return "TransitionType({}, {}, {})".format(
- self._offset, self._is_dst, self._abbr
- )
diff --git a/pendulum/utils/_compat.py b/pendulum/utils/_compat.py
index 07cead1..8f32f9e 100644
--- a/pendulum/utils/_compat.py
+++ b/pendulum/utils/_compat.py
@@ -1,54 +1,13 @@
-import sys
-
-
-PY2 = sys.version_info < (3, 0)
-PY36 = sys.version_info >= (3, 6)
-PYPY = hasattr(sys, "pypy_version_info")
-
-_HAS_FOLD = PY36
-
-
-try: # Python 2
- long = long
- unicode = unicode
- basestring = basestring
-except NameError: # Python 3
- long = int
- unicode = str
- basestring = str
-
-
-def decode(string, encodings=None):
- if not PY2 and not isinstance(string, bytes):
- return string
-
- if PY2 and isinstance(string, unicode):
- return string
-
- encodings = encodings or ["utf-8", "latin1", "ascii"]
-
- for encoding in encodings:
- try:
- return string.decode(encoding)
- except (UnicodeEncodeError, UnicodeDecodeError):
- pass
-
- return string.decode(encodings[0], errors="ignore")
-
-
-def encode(string, encodings=None):
- if not PY2 and isinstance(string, bytes):
- return string
-
- if PY2 and isinstance(string, str):
- return string
-
- encodings = encodings or ["utf-8", "latin1", "ascii"]
-
- for encoding in encodings:
- try:
- return string.encode(encoding)
- except (UnicodeEncodeError, UnicodeDecodeError):
- pass
-
- return string.encode(encodings[0], errors="ignore")
+from __future__ import annotations
+
+import sys
+
+PYPY = hasattr(sys, "pypy_version_info")
+PY38 = sys.version_info[:2] >= (3, 8)
+
+try:
+ from backports import zoneinfo
+except ImportError:
+ import zoneinfo # type: ignore[no-redef]
+
+__all__ = ["zoneinfo"]
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..03a2975
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,1172 @@
+# This file is automatically @generated by Poetry and should not be changed by hand.
+
+[[package]]
+name = "atomicwrites"
+version = "1.4.1"
+description = "Atomic file writes."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "attrs"
+version = "22.1.0"
+description = "Classes Without Boilerplate"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.extras]
+dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
+docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
+tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
+tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
+
+[[package]]
+name = "babel"
+version = "2.10.3"
+description = "Internationalization utilities"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pytz = ">=2015.7"
+
+[[package]]
+name = "backports-zoneinfo"
+version = "0.2.1"
+description = "Backport of the standard library zoneinfo module"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+tzdata = ["tzdata"]
+
+[[package]]
+name = "black"
+version = "22.6.0"
+description = "The uncompromising code formatter."
+category = "dev"
+optional = false
+python-versions = ">=3.6.2"
+
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
+typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
+typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.7.4)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "cfgv"
+version = "3.3.1"
+description = "Validate configuration and produce human readable error messages."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[[package]]
+name = "cleo"
+version = "1.0.0a5"
+description = "Cleo allows you to create beautiful and testable command-line interfaces."
+category = "dev"
+optional = false
+python-versions = ">=3.7,<4.0"
+
+[package.dependencies]
+crashtest = ">=0.3.1,<0.4.0"
+pylev = ">=1.3.0,<2.0.0"
+
+[[package]]
+name = "click"
+version = "8.1.3"
+description = "Composable command line interface toolkit"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.5"
+description = "Cross-platform colored terminal text."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "coverage"
+version = "6.4.2"
+description = "Code coverage measurement for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "crashtest"
+version = "0.3.1"
+description = "Manage Python errors with ease"
+category = "dev"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[[package]]
+name = "distlib"
+version = "0.3.5"
+description = "Distribution utilities"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "filelock"
+version = "3.7.1"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
+testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"]
+
+[[package]]
+name = "ghp-import"
+version = "2.1.0"
+description = "Copy your docs directly to the gh-pages branch."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+python-dateutil = ">=2.8.1"
+
+[package.extras]
+dev = ["flake8", "markdown", "twine", "wheel"]
+
+[[package]]
+name = "identify"
+version = "2.5.3"
+description = "File identification library for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+license = ["ukkonen"]
+
+[[package]]
+name = "importlib-metadata"
+version = "4.12.0"
+description = "Read metadata from Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
+zipp = ">=0.5"
+
+[package.extras]
+docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"]
+perf = ["ipython"]
+testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
+
+[[package]]
+name = "importlib-resources"
+version = "5.9.0"
+description = "Read resources from Python packages"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
+
+[package.extras]
+docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"]
+testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+
+[[package]]
+name = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "isort"
+version = "5.10.1"
+description = "A Python utility / library to sort Python imports."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1,<4.0"
+
+[package.extras]
+colors = ["colorama (>=0.4.3,<0.5.0)"]
+pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
+plugins = ["setuptools"]
+requirements_deprecated_finder = ["pip-api", "pipreqs"]
+
+[[package]]
+name = "jinja2"
+version = "3.1.2"
+description = "A very fast and expressive template engine."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
+[[package]]
+name = "markdown"
+version = "3.4.1"
+description = "Python implementation of Markdown."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
+
+[package.extras]
+testing = ["coverage", "pyyaml"]
+
+[[package]]
+name = "markdown-include"
+version = "0.5.1"
+description = "This is an extension to Python-Markdown which provides an \"include\" function, similar to that found in LaTeX (and also the C pre-processor and Fortran). I originally wrote it for my FORD Fortran auto-documentation generator."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+markdown = "*"
+
+[[package]]
+name = "markupsafe"
+version = "2.1.1"
+description = "Safely add untrusted strings to HTML/XML markup."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "mergedeep"
+version = "1.3.4"
+description = "A deep merge function for 🐍."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "meson"
+version = "0.63.2"
+description = "A high performance build system"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+ninja = ["ninja (>=1.8.2)"]
+progress = ["tqdm"]
+typing = ["mypy", "typing-extensions"]
+
+[[package]]
+name = "mkdocs"
+version = "1.3.0"
+description = "Project documentation with Markdown."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+click = ">=3.3"
+ghp-import = ">=1.0"
+importlib-metadata = ">=4.3"
+Jinja2 = ">=2.10.2"
+Markdown = ">=3.2.1"
+mergedeep = ">=1.3.4"
+packaging = ">=20.5"
+PyYAML = ">=3.10"
+pyyaml-env-tag = ">=0.1"
+watchdog = ">=2.0"
+
+[package.extras]
+i18n = ["babel (>=2.9.0)"]
+
+[[package]]
+name = "mypy-extensions"
+version = "0.4.3"
+description = "Experimental type system extensions for programs checked with the mypy typechecker."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "ninja"
+version = "1.10.2.3"
+description = "Ninja is a small build system with a focus on speed"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+test = ["codecov (>=2.0.5)", "coverage (>=4.2)", "flake8 (>=3.0.4)", "pytest (>=4.5.0)", "pytest-cov (>=2.7.1)", "pytest-runner (>=5.1)", "pytest-virtualenv (>=1.7.0)", "virtualenv (>=15.0.3)"]
+
+[[package]]
+name = "nodeenv"
+version = "1.7.0"
+description = "Node.js virtual environment builder"
+category = "dev"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+
+[package.dependencies]
+setuptools = "*"
+
+[[package]]
+name = "packaging"
+version = "21.3"
+description = "Core utilities for Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
+
+[[package]]
+name = "pathspec"
+version = "0.9.0"
+description = "Utility library for gitignore style pattern matching of file paths."
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[[package]]
+name = "platformdirs"
+version = "2.5.2"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
+test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
+
+[[package]]
+name = "pluggy"
+version = "1.0.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pre-commit"
+version = "2.20.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+toml = "*"
+virtualenv = ">=20.0.8"
+
+[[package]]
+name = "py"
+version = "1.11.0"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "pygments"
+version = "2.12.0"
+description = "Pygments is a syntax highlighting package written in Python."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "pylev"
+version = "1.4.0"
+description = "A pure Python Levenshtein implementation that's not freaking GPL'd."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pymdown-extensions"
+version = "6.3"
+description = "Extension pack for Python Markdown."
+category = "dev"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
+
+[package.dependencies]
+Markdown = ">=3.2"
+
+[[package]]
+name = "pyparsing"
+version = "3.0.9"
+description = "pyparsing module - Classes and methods to define and execute parsing grammars"
+category = "dev"
+optional = false
+python-versions = ">=3.6.8"
+
+[package.extras]
+diagrams = ["jinja2", "railroad-diagrams"]
+
+[[package]]
+name = "pytest"
+version = "7.1.2"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+py = ">=1.8.2"
+tomli = ">=1.0.0"
+
+[package.extras]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+
+[[package]]
+name = "pytest-cov"
+version = "3.0.0"
+description = "Pytest plugin for measuring coverage."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+coverage = {version = ">=5.2.1", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
+
+[[package]]
+name = "python-dateutil"
+version = "2.8.2"
+description = "Extensions to the standard Python datetime module"
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "pytz"
+version = "2022.1"
+description = "World timezone definitions, modern and historical"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pyyaml"
+version = "6.0"
+description = "YAML parser and emitter for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "pyyaml-env-tag"
+version = "0.1"
+description = "A custom YAML tag for referencing environment variables in YAML files. "
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pyyaml = "*"
+
+[[package]]
+name = "setuptools"
+version = "63.4.1"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "time-machine"
+version = "2.7.1"
+description = "Travel through time in your tests."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+python-dateutil = "*"
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "tox"
+version = "3.25.1"
+description = "tox is a generic virtualenv management and test command line tool"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[package.dependencies]
+colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""}
+filelock = ">=3.0.0"
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+packaging = ">=14"
+pluggy = ">=0.12.0"
+py = ">=1.4.17"
+six = ">=1.14.0"
+toml = ">=0.9.4"
+virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7"
+
+[package.extras]
+docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"]
+testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"]
+
+[[package]]
+name = "typed-ast"
+version = "1.5.4"
+description = "a fork of Python 2 and 3 ast modules with type comment support"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "types-backports"
+version = "0.1.3"
+description = "Typing stubs for backports"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "types-python-dateutil"
+version = "2.8.19"
+description = "Typing stubs for python-dateutil"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "typing-extensions"
+version = "4.3.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "tzdata"
+version = "2022.1"
+description = "Provider of IANA time zone data"
+category = "main"
+optional = false
+python-versions = ">=2"
+
+[[package]]
+name = "virtualenv"
+version = "20.16.3"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+distlib = ">=0.3.5,<1"
+filelock = ">=3.4.1,<4"
+importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""}
+platformdirs = ">=2.4,<3"
+
+[package.extras]
+docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"]
+testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"]
+
+[[package]]
+name = "watchdog"
+version = "2.1.9"
+description = "Filesystem events monitoring"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+watchmedo = ["PyYAML (>=3.10)"]
+
+[[package]]
+name = "zipp"
+version = "3.8.1"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"]
+testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+
+[metadata]
+lock-version = "1.1"
+python-versions = "^3.7"
+content-hash = "d6cae20188419d1a859377f888c81aa3a057dd11104671bc2cc9831a30c1a9c1"
+
+[metadata.files]
+atomicwrites = [
+ {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"},
+]
+attrs = [
+ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
+ {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
+]
+babel = [
+ {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"},
+ {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"},
+]
+backports-zoneinfo = [
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"},
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"},
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"},
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"},
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"},
+ {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"},
+]
+black = [
+ {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"},
+ {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"},
+ {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"},
+ {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"},
+ {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"},
+ {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"},
+ {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"},
+ {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"},
+ {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"},
+ {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"},
+ {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"},
+ {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"},
+ {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"},
+ {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"},
+ {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"},
+ {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"},
+ {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"},
+ {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"},
+ {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"},
+ {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"},
+ {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"},
+ {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"},
+ {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"},
+]
+cfgv = [
+ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
+ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
+]
+cleo = [
+ {file = "cleo-1.0.0a5-py3-none-any.whl", hash = "sha256:ff53056589300976e960f75afb792dfbfc9c78dcbb5a448e207a17b643826360"},
+ {file = "cleo-1.0.0a5.tar.gz", hash = "sha256:097c9d0e0332fd53cc89fc11eb0a6ba0309e6a3933c08f7b38558555486925d3"},
+]
+click = [
+ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
+ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
+]
+colorama = [
+ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
+ {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
+]
+coverage = [
+ {file = "coverage-6.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a9032f9b7d38bdf882ac9f66ebde3afb8145f0d4c24b2e600bc4c6304aafb87e"},
+ {file = "coverage-6.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e0524adb49c716ca763dbc1d27bedce36b14f33e6b8af6dba56886476b42957c"},
+ {file = "coverage-6.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4548be38a1c810d79e097a38107b6bf2ff42151900e47d49635be69943763d8"},
+ {file = "coverage-6.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f23876b018dfa5d3e98e96f5644b109090f16a4acb22064e0f06933663005d39"},
+ {file = "coverage-6.4.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fe75dcfcb889b6800f072f2af5a331342d63d0c1b3d2bf0f7b4f6c353e8c9c0"},
+ {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2f8553878a24b00d5ab04b7a92a2af50409247ca5c4b7a2bf4eabe94ed20d3ee"},
+ {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d774d9e97007b018a651eadc1b3970ed20237395527e22cbeb743d8e73e0563d"},
+ {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d56f105592188ce7a797b2bd94b4a8cb2e36d5d9b0d8a1d2060ff2a71e6b9bbc"},
+ {file = "coverage-6.4.2-cp310-cp310-win32.whl", hash = "sha256:d230d333b0be8042ac34808ad722eabba30036232e7a6fb3e317c49f61c93386"},
+ {file = "coverage-6.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:5ef42e1db047ca42827a85e34abe973971c635f83aed49611b7f3ab49d0130f0"},
+ {file = "coverage-6.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:25b7ec944f114f70803d6529394b64f8749e93cbfac0fe6c5ea1b7e6c14e8a46"},
+ {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bb00521ab4f99fdce2d5c05a91bddc0280f0afaee0e0a00425e28e209d4af07"},
+ {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dff52b3e7f76ada36f82124703f4953186d9029d00d6287f17c68a75e2e6039"},
+ {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147605e1702d996279bb3cc3b164f408698850011210d133a2cb96a73a2f7996"},
+ {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:422fa44070b42fef9fb8dabd5af03861708cdd6deb69463adc2130b7bf81332f"},
+ {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8af6c26ba8df6338e57bedbf916d76bdae6308e57fc8f14397f03b5da8622b4e"},
+ {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5336e0352c0b12c7e72727d50ff02557005f79a0b8dcad9219c7c4940a930083"},
+ {file = "coverage-6.4.2-cp37-cp37m-win32.whl", hash = "sha256:0f211df2cba951ffcae210ee00e54921ab42e2b64e0bf2c0befc977377fb09b7"},
+ {file = "coverage-6.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a13772c19619118903d65a91f1d5fea84be494d12fd406d06c849b00d31bf120"},
+ {file = "coverage-6.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7bd0ffbcd03dc39490a1f40b2669cc414fae0c4e16b77bb26806a4d0b7d1452"},
+ {file = "coverage-6.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0895ea6e6f7f9939166cc835df8fa4599e2d9b759b02d1521b574e13b859ac32"},
+ {file = "coverage-6.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e7ced84a11c10160c0697a6cc0b214a5d7ab21dfec1cd46e89fbf77cc66fae"},
+ {file = "coverage-6.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80db4a47a199c4563d4a25919ff29c97c87569130375beca3483b41ad5f698e8"},
+ {file = "coverage-6.4.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3def6791adf580d66f025223078dc84c64696a26f174131059ce8e91452584e1"},
+ {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4f89d8e03c8a3757aae65570d14033e8edf192ee9298303db15955cadcff0c63"},
+ {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6d0b48aff8e9720bdec315d67723f0babd936a7211dc5df453ddf76f89c59933"},
+ {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2b20286c2b726f94e766e86a3fddb7b7e37af5d0c635bdfa7e4399bc523563de"},
+ {file = "coverage-6.4.2-cp38-cp38-win32.whl", hash = "sha256:d714af0bdba67739598849c9f18efdcc5a0412f4993914a0ec5ce0f1e864d783"},
+ {file = "coverage-6.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:5f65e5d3ff2d895dab76b1faca4586b970a99b5d4b24e9aafffc0ce94a6022d6"},
+ {file = "coverage-6.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a697977157adc052284a7160569b36a8bbec09db3c3220642e6323b47cec090f"},
+ {file = "coverage-6.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c77943ef768276b61c96a3eb854eba55633c7a3fddf0a79f82805f232326d33f"},
+ {file = "coverage-6.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54d8d0e073a7f238f0666d3c7c0d37469b2aa43311e4024c925ee14f5d5a1cbe"},
+ {file = "coverage-6.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22325010d8824594820d6ce84fa830838f581a7fd86a9235f0d2ed6deb61e29"},
+ {file = "coverage-6.4.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24b04d305ea172ccb21bee5bacd559383cba2c6fcdef85b7701cf2de4188aa55"},
+ {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:866ebf42b4c5dbafd64455b0a1cd5aa7b4837a894809413b930026c91e18090b"},
+ {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e36750fbbc422c1c46c9d13b937ab437138b998fe74a635ec88989afb57a3978"},
+ {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:79419370d6a637cb18553ecb25228893966bd7935a9120fa454e7076f13b627c"},
+ {file = "coverage-6.4.2-cp39-cp39-win32.whl", hash = "sha256:b5e28db9199dd3833cc8a07fa6cf429a01227b5d429facb56eccd765050c26cd"},
+ {file = "coverage-6.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:edfdabe7aa4f97ed2b9dd5dde52d2bb29cb466993bb9d612ddd10d0085a683cf"},
+ {file = "coverage-6.4.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:e2618cb2cf5a7cc8d698306e42ebcacd02fb7ef8cfc18485c59394152c70be97"},
+ {file = "coverage-6.4.2.tar.gz", hash = "sha256:6c3ccfe89c36f3e5b9837b9ee507472310164f352c9fe332120b764c9d60adbe"},
+]
+crashtest = [
+ {file = "crashtest-0.3.1-py3-none-any.whl", hash = "sha256:300f4b0825f57688b47b6d70c6a31de33512eb2fa1ac614f780939aa0cf91680"},
+ {file = "crashtest-0.3.1.tar.gz", hash = "sha256:42ca7b6ce88b6c7433e2ce47ea884e91ec93104a4b754998be498a8e6c3d37dd"},
+]
+distlib = [
+ {file = "distlib-0.3.5-py2.py3-none-any.whl", hash = "sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c"},
+ {file = "distlib-0.3.5.tar.gz", hash = "sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe"},
+]
+filelock = [
+ {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"},
+ {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"},
+]
+ghp-import = [
+ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
+ {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
+]
+identify = [
+ {file = "identify-2.5.3-py2.py3-none-any.whl", hash = "sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893"},
+ {file = "identify-2.5.3.tar.gz", hash = "sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44"},
+]
+importlib-metadata = [
+ {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"},
+ {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"},
+]
+importlib-resources = [
+ {file = "importlib_resources-5.9.0-py3-none-any.whl", hash = "sha256:f78a8df21a79bcc30cfd400bdc38f314333de7c0fb619763f6b9dabab8268bb7"},
+ {file = "importlib_resources-5.9.0.tar.gz", hash = "sha256:5481e97fb45af8dcf2f798952625591c58fe599d0735d86b10f54de086a61681"},
+]
+iniconfig = [
+ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
+isort = [
+ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
+ {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
+]
+jinja2 = [
+ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
+ {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
+]
+markdown = [
+ {file = "Markdown-3.4.1-py3-none-any.whl", hash = "sha256:08fb8465cffd03d10b9dd34a5c3fea908e20391a2a90b88d66362cb05beed186"},
+ {file = "Markdown-3.4.1.tar.gz", hash = "sha256:3b809086bb6efad416156e00a0da66fe47618a5d6918dd688f53f40c8e4cfeff"},
+]
+markdown-include = [
+ {file = "markdown-include-0.5.1.tar.gz", hash = "sha256:72a45461b589489a088753893bc95c5fa5909936186485f4ed55caa57d10250f"},
+]
+markupsafe = [
+ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
+ {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
+]
+mergedeep = [
+ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
+ {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
+]
+meson = [
+ {file = "meson-0.63.2-py3-none-any.whl", hash = "sha256:64a83ef257b2962b52c8b07ad9ec536c2de1b72fd9f14bcd9c21fe45730edd46"},
+ {file = "meson-0.63.2.tar.gz", hash = "sha256:16222f17ef76be0542c91c07994f9676ae879f46fc21c0c786a21ef2cb518bbf"},
+]
+mkdocs = [
+ {file = "mkdocs-1.3.0-py3-none-any.whl", hash = "sha256:26bd2b03d739ac57a3e6eed0b7bcc86168703b719c27b99ad6ca91dc439aacde"},
+ {file = "mkdocs-1.3.0.tar.gz", hash = "sha256:b504405b04da38795fec9b2e5e28f6aa3a73bb0960cb6d5d27ead28952bd35ea"},
+]
+mypy-extensions = [
+ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
+ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
+]
+ninja = [
+ {file = "ninja-1.10.2.3-py2.py3-none-macosx_10_9_universal2.macosx_10_9_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:d5e0275d28997a750a4f445c00bdd357b35cc334c13cdff13edf30e544704fbd"},
+ {file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ea785bf6a15727040835256577239fa3cf5da0d60e618c307aa5efc31a1f0ce"},
+ {file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29570a18d697fc84d361e7e6330f0021f34603ae0fcb0ef67ae781e9814aae8d"},
+ {file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21a1d84d4c7df5881bfd86c25cce4cf7af44ba2b8b255c57bc1c434ec30a2dfc"},
+ {file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9ca8dbece144366d5f575ffc657af03eb11c58251268405bc8519d11cf42f113"},
+ {file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:279836285975e3519392c93c26e75755e8a8a7fafec9f4ecbb0293119ee0f9c6"},
+ {file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cc8b31b5509a2129e4d12a35fc21238c157038022560aaf22e49ef0a77039086"},
+ {file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_i686.whl", hash = "sha256:688167841b088b6802e006f911d911ffa925e078c73e8ef2f88286107d3204f8"},
+ {file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:840a0b042d43a8552c4004966e18271ec726e5996578f28345d9ce78e225b67e"},
+ {file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_s390x.whl", hash = "sha256:84be6f9ec49f635dc40d4b871319a49fa49b8d55f1d9eae7cd50d8e57ddf7a85"},
+ {file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6bd76a025f26b9ae507cf8b2b01bb25bb0031df54ed685d85fc559c411c86cf4"},
+ {file = "ninja-1.10.2.3-py2.py3-none-win32.whl", hash = "sha256:740d61fefb4ca13573704ee8fe89b973d40b8dc2a51aaa4e9e68367233743bb6"},
+ {file = "ninja-1.10.2.3-py2.py3-none-win_amd64.whl", hash = "sha256:0560eea57199e41e86ac2c1af0108b63ae77c3ca4d05a9425a750e908135935a"},
+ {file = "ninja-1.10.2.3.tar.gz", hash = "sha256:e1b86ad50d4e681a7dbdff05fc23bb52cb773edb90bc428efba33fa027738408"},
+]
+nodeenv = [
+ {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
+ {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
+]
+packaging = [
+ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
+ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
+]
+pathspec = [
+ {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
+ {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
+]
+platformdirs = [
+ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
+ {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
+]
+pluggy = [
+ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
+ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+]
+pre-commit = [
+ {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"},
+ {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"},
+]
+py = [
+ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
+ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
+]
+pygments = [
+ {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"},
+ {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"},
+]
+pylev = [
+ {file = "pylev-1.4.0-py2.py3-none-any.whl", hash = "sha256:7b2e2aa7b00e05bb3f7650eb506fc89f474f70493271a35c242d9a92188ad3dd"},
+ {file = "pylev-1.4.0.tar.gz", hash = "sha256:9e77e941042ad3a4cc305dcdf2b2dec1aec2fbe3dd9015d2698ad02b173006d1"},
+]
+pymdown-extensions = [
+ {file = "pymdown-extensions-6.3.tar.gz", hash = "sha256:cb879686a586b22292899771f5e5bc3382808e92aa938f71b550ecdea709419f"},
+ {file = "pymdown_extensions-6.3-py2.py3-none-any.whl", hash = "sha256:66fae2683c7a1dac53184f7de57f51f8dad73f9ead2f453e94e85096cb811335"},
+]
+pyparsing = [
+ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
+ {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
+]
+pytest = [
+ {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
+ {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"},
+]
+pytest-cov = [
+ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
+ {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
+]
+python-dateutil = [
+ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
+ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
+]
+pytz = [
+ {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
+ {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"},
+]
+pyyaml = [
+ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
+ {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
+ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
+ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
+ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
+ {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
+ {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
+ {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
+ {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
+ {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
+ {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
+ {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
+ {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
+ {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
+ {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
+ {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
+ {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
+ {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
+ {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
+ {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
+ {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
+ {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
+ {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
+ {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
+ {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
+ {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
+ {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
+ {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
+ {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
+ {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
+ {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
+ {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
+ {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
+ {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
+ {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
+ {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
+ {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
+ {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
+ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
+ {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
+]
+pyyaml-env-tag = [
+ {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
+ {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
+]
+setuptools = [
+ {file = "setuptools-63.4.1-py3-none-any.whl", hash = "sha256:dc2662692f47d99cb8ae15a784529adeed535bcd7c277fee0beccf961522baf6"},
+ {file = "setuptools-63.4.1.tar.gz", hash = "sha256:7c7854ee1429a240090297628dc9f75b35318d193537968e2dc14010ee2f5bca"},
+]
+six = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+time-machine = [
+ {file = "time-machine-2.7.1.tar.gz", hash = "sha256:be6c1f0421a77a046db8fae00886fb364f683a86612b71dd5c74b22891590042"},
+ {file = "time_machine-2.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ae93d2f761435d192bc80c148438a0c4261979db0610cef08dfe2c8d21ca1c67"},
+ {file = "time_machine-2.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:342b431154fbfb1889f8d7aa3d857373a837106bba395a5cc99123f11a7cea03"},
+ {file = "time_machine-2.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4011ea76f6ad2f932f00cf9e77a25b575a024d6bc15bcf891a3f9916ceeb6e"},
+ {file = "time_machine-2.7.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43ae8192d370a90d2246fca565a55633f592b314264c65c5c9151c361b715fb9"},
+ {file = "time_machine-2.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cea12d0592ebbe738db952ce6fd272ed90e7bbb095e802f4f2145f8f0e322fa3"},
+ {file = "time_machine-2.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:732d5fd2d442fa87538b5a6ca623cb205b9b048d2c9aaf79e5cfc7ec7f637848"},
+ {file = "time_machine-2.7.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c34e1f49cad2fd41d42c4aabd3d69a32c79d9a8e0779064554843823cd1fb1e4"},
+ {file = "time_machine-2.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6af3e81cf663b6d5660953ae59da2bb2ae802452ecbc9907272979ed06253659"},
+ {file = "time_machine-2.7.1-cp310-cp310-win32.whl", hash = "sha256:10c2937d3556f4358205dac5c7cd2d33832b8b911f3deff050f59e1fe2be3231"},
+ {file = "time_machine-2.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:200974e9bb8a1cb227ce579caafeaeebb0f9de81758c444cbccc0ea464313caf"},
+ {file = "time_machine-2.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d5e2376b7922c9d96921709c7e730498b9c69da889f359a465d0c43117b62da3"},
+ {file = "time_machine-2.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ad9117abe223cdc7b4a4432e0a0cfebb1b351a091ee996c653e90f27a734fce"},
+ {file = "time_machine-2.7.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:626ef686723147468e84da3edcd67ff757a463250fd35f8f6a8e5b899c43b43d"},
+ {file = "time_machine-2.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9331946ed13acd50bc484f408e26b8eefa67e3dbca41927d2052f2148d3661d"},
+ {file = "time_machine-2.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3d0612e0323047f29c23732963d9926f1a95e2ce334d86fecd37c803ac240fc6"},
+ {file = "time_machine-2.7.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b474499ad0083252240bc5be13f8116cc2ca8a89d1ca4967ed74a7b5f0883f95"},
+ {file = "time_machine-2.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7df0857709432585b62d2667c0e6e64029b652e2df776b9fb85223c60dce52c7"},
+ {file = "time_machine-2.7.1-cp37-cp37m-win32.whl", hash = "sha256:77c8dfe8dc7f45bbfe73494c72f3728d99abec5a020460ad7ffee5247365eba4"},
+ {file = "time_machine-2.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c1fd1c231377ce076f99c8c16999a95510690f8dbd35db0e5fbbc74a17f84b39"},
+ {file = "time_machine-2.7.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:462924fb87826882fc7830098e621116599f9259d181a7bbf5a4e49f74ec325b"},
+ {file = "time_machine-2.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:46bf3b4a52d43289b23f0015a9d8592ddf621a5058e566c275cb060347d430c1"},
+ {file = "time_machine-2.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30afd5b978d8121334c80fa23119d7bd7c9f954169854edf5103e5c8b38358bb"},
+ {file = "time_machine-2.7.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:633fb8c47f3cd64690591ca6981e4fdbcaa54c18d8a57a3cdc24638ca98f8216"},
+ {file = "time_machine-2.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b6093c3b70d1d1a66b65f18a6e53b233c8dd5d8ffe7ac59e9d048fb1d5e15c"},
+ {file = "time_machine-2.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e62ed7d78694b7e0a2ab30b3dd52ebf26b03e17d6eda0f231fd77e24307a55a9"},
+ {file = "time_machine-2.7.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0eaf024d16482ec211a579fd389cbbd4fedd8a1f0a0c41642508815f880ca3a9"},
+ {file = "time_machine-2.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2688091ce0c16151faa80625efb34e3096731fbdee6d5284c48c984bce95c311"},
+ {file = "time_machine-2.7.1-cp38-cp38-win32.whl", hash = "sha256:2e54bf0521b6e397fcaa03060feb187bbe5aa63ac51dbb97d5bc59fb0c4725f8"},
+ {file = "time_machine-2.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:cee72d9e14d36e4b8da6af1d2d784f14da53f76aeb5066540a38318aa907b551"},
+ {file = "time_machine-2.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:06322d41d45d86e2dc2520794c95129ff25b8620b33851ed40700c859ebf8c30"},
+ {file = "time_machine-2.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:193b14daa3b3cf67e6b55d6e2d63c2eb7c1d3f49017704d4b43963b198656888"},
+ {file = "time_machine-2.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1367a89fb857f68cfa723e236cd47febaf201a3a625ad8423110fe0509d5fca8"},
+ {file = "time_machine-2.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce350f7e8bd51a0bb064180486300283bec5cd1a21a318a8ffe5f7df11735f36"},
+ {file = "time_machine-2.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68ff623d835760314e279aedc0d19a1dc4dec117c6bca388e1ff077c781256bd"},
+ {file = "time_machine-2.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:05fecd818d41727d31109a0d039ce07c8311602b45ffc07bffd8ae8b6f266ee5"},
+ {file = "time_machine-2.7.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1fe4e604c5effc290c1bbecd3ea98687690d0a88fd98ba93e0246bf19ae2a520"},
+ {file = "time_machine-2.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff07a5635d42957f2bd7eb5ca6579f64de368c842e754a4d3414520693b75db9"},
+ {file = "time_machine-2.7.1-cp39-cp39-win32.whl", hash = "sha256:8c6314e7e0ffd7af82c8026786d5551aff973e0c86ec1368b0590be9a7620cad"},
+ {file = "time_machine-2.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:d50a2620d726788cbde97c58e0f6f61d10337d16d088a1fad789f50a1b5ff4d1"},
+]
+toml = [
+ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+tomli = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+tox = [
+ {file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"},
+ {file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"},
+]
+typed-ast = [
+ {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
+ {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
+ {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"},
+ {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"},
+ {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"},
+ {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"},
+ {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"},
+ {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"},
+ {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"},
+ {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"},
+ {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"},
+ {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"},
+ {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"},
+ {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"},
+ {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"},
+ {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"},
+ {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"},
+ {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"},
+ {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"},
+ {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"},
+ {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"},
+ {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"},
+ {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
+ {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
+]
+types-backports = [
+ {file = "types-backports-0.1.3.tar.gz", hash = "sha256:f4b7206c073df88d6200891e3d27506185fd60cda66fb289737b2fa92c0010cf"},
+ {file = "types_backports-0.1.3-py2.py3-none-any.whl", hash = "sha256:dafcd61848081503e738a7768872d1dd6c018401b4d2a1cfb608ea87ec9864b9"},
+]
+types-python-dateutil = [
+ {file = "types-python-dateutil-2.8.19.tar.gz", hash = "sha256:bfd3eb39c7253aea4ba23b10f69b017d30b013662bb4be4ab48b20bbd763f309"},
+ {file = "types_python_dateutil-2.8.19-py3-none-any.whl", hash = "sha256:6284df1e4783d8fc6e587f0317a81333856b872a6669a282f8a325342bce7fa8"},
+]
+typing-extensions = [
+ {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
+ {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
+]
+tzdata = [
+ {file = "tzdata-2022.1-py2.py3-none-any.whl", hash = "sha256:238e70234214138ed7b4e8a0fab0e5e13872edab3be586ab8198c407620e2ab9"},
+ {file = "tzdata-2022.1.tar.gz", hash = "sha256:8b536a8ec63dc0751342b3984193a3118f8fca2afe25752bb9b7fffd398552d3"},
+]
+virtualenv = [
+ {file = "virtualenv-20.16.3-py2.py3-none-any.whl", hash = "sha256:4193b7bc8a6cd23e4eb251ac64f29b4398ab2c233531e66e40b19a6b7b0d30c1"},
+ {file = "virtualenv-20.16.3.tar.gz", hash = "sha256:d86ea0bb50e06252d79e6c241507cb904fcd66090c3271381372d6221a3970f9"},
+]
+watchdog = [
+ {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"},
+ {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d"},
+ {file = "watchdog-2.1.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee3e38a6cc050a8830089f79cbec8a3878ec2fe5160cdb2dc8ccb6def8552658"},
+ {file = "watchdog-2.1.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64a27aed691408a6abd83394b38503e8176f69031ca25d64131d8d640a307591"},
+ {file = "watchdog-2.1.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:195fc70c6e41237362ba720e9aaf394f8178bfc7fa68207f112d108edef1af33"},
+ {file = "watchdog-2.1.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bfc4d351e6348d6ec51df007432e6fe80adb53fd41183716017026af03427846"},
+ {file = "watchdog-2.1.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8250546a98388cbc00c3ee3cc5cf96799b5a595270dfcfa855491a64b86ef8c3"},
+ {file = "watchdog-2.1.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:117ffc6ec261639a0209a3252546b12800670d4bf5f84fbd355957a0595fe654"},
+ {file = "watchdog-2.1.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:97f9752208f5154e9e7b76acc8c4f5a58801b338de2af14e7e181ee3b28a5d39"},
+ {file = "watchdog-2.1.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:247dcf1df956daa24828bfea5a138d0e7a7c98b1a47cf1fa5b0c3c16241fcbb7"},
+ {file = "watchdog-2.1.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:226b3c6c468ce72051a4c15a4cc2ef317c32590d82ba0b330403cafd98a62cfd"},
+ {file = "watchdog-2.1.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d9820fe47c20c13e3c9dd544d3706a2a26c02b2b43c993b62fcd8011bcc0adb3"},
+ {file = "watchdog-2.1.9-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:70af927aa1613ded6a68089a9262a009fbdf819f46d09c1a908d4b36e1ba2b2d"},
+ {file = "watchdog-2.1.9-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed80a1628cee19f5cfc6bb74e173f1b4189eb532e705e2a13e3250312a62e0c9"},
+ {file = "watchdog-2.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9f05a5f7c12452f6a27203f76779ae3f46fa30f1dd833037ea8cbc2887c60213"},
+ {file = "watchdog-2.1.9-py3-none-manylinux2014_armv7l.whl", hash = "sha256:255bb5758f7e89b1a13c05a5bceccec2219f8995a3a4c4d6968fe1de6a3b2892"},
+ {file = "watchdog-2.1.9-py3-none-manylinux2014_i686.whl", hash = "sha256:d3dda00aca282b26194bdd0adec21e4c21e916956d972369359ba63ade616153"},
+ {file = "watchdog-2.1.9-py3-none-manylinux2014_ppc64.whl", hash = "sha256:186f6c55abc5e03872ae14c2f294a153ec7292f807af99f57611acc8caa75306"},
+ {file = "watchdog-2.1.9-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:083171652584e1b8829581f965b9b7723ca5f9a2cd7e20271edf264cfd7c1412"},
+ {file = "watchdog-2.1.9-py3-none-manylinux2014_s390x.whl", hash = "sha256:b530ae007a5f5d50b7fbba96634c7ee21abec70dc3e7f0233339c81943848dc1"},
+ {file = "watchdog-2.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:4f4e1c4aa54fb86316a62a87b3378c025e228178d55481d30d857c6c438897d6"},
+ {file = "watchdog-2.1.9-py3-none-win32.whl", hash = "sha256:5952135968519e2447a01875a6f5fc8c03190b24d14ee52b0f4b1682259520b1"},
+ {file = "watchdog-2.1.9-py3-none-win_amd64.whl", hash = "sha256:7a833211f49143c3d336729b0020ffd1274078e94b0ae42e22f596999f50279c"},
+ {file = "watchdog-2.1.9-py3-none-win_ia64.whl", hash = "sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428"},
+ {file = "watchdog-2.1.9.tar.gz", hash = "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609"},
+]
+zipp = [
+ {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"},
+ {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"},
+]
diff --git a/pyproject.toml b/pyproject.toml
index cadf000..a61cae0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,81 +1,183 @@
-[tool.poetry]
-name = "pendulum"
-version = "2.1.2"
-description = "Python datetimes made easy"
-authors = ["Sébastien Eustace <sebastien@eustace.io>"]
-license = "MIT"
-readme = 'README.rst'
-homepage = "https://pendulum.eustace.io"
-repository = "https://github.com/sdispater/pendulum"
-documentation = "https://pendulum.eustace.io/docs"
-keywords = ['datetime', 'date', 'time']
-
-packages = [
- {include = "pendulum"},
- #{include = "tests", format = "sdist"},
-]
-include = [
- {path = "pendulum/py.typed"},
- # C extensions must be included in the wheel distributions
- {path = "pendulum/_extensions/*.so", format = "wheel"},
- {path = "pendulum/_extensions/*.pyd", format = "wheel"},
- {path = "pendulum/parsing/*.so", format = "wheel"},
- {path = "pendulum/parsing/*.pyd", format = "wheel"},
-]
-
-
-[tool.poetry.dependencies]
-python = "~2.7 || ^3.5"
-python-dateutil = "^2.6"
-pytzdata = ">=2020.1"
-
-# typing is needed for Python < 3.5
-typing = { version = "^3.6", python = "<3.5" }
-
-[tool.poetry.dev-dependencies]
-pytest = "^4.6"
-pytest-cov = "^2.5"
-pytz = ">=2018.3"
-babel = "^2.5"
-cleo = "^0.8.1"
-tox = "^3.0"
-black = { version = "^19.3b0", markers = "python_version >= '3.6' and python_version < '4.0' and implementation_name != 'pypy'" }
-isort = { version = "^4.3.21", markers = "python_version >= '3.6' and python_version < '4.0'" }
-pre-commit = "^1.10"
-mkdocs = { version = "^1.0", python = "^3.5" }
-pymdown-extensions = "^6.0"
-pygments = "^2.2"
-markdown-include = "^0.5.1"
-freezegun = "^0.3.15"
-
-[tool.poetry.build]
-generate-setup-file = false
-script = "build.py"
-
-[tool.isort]
-line_length = 88
-force_single_line = true
-force_grid_wrap = 0
-atomic = true
-include_trailing_comma = true
-lines_after_imports = 2
-lines_between_types = 1
-multi_line_output = 3
-use_parentheses = true
-not_skip = "__init__.py"
-skip_glob = ["*/setup.py"]
-filter_files = true
-
-known_first_party = "pendulum"
-known_third_party = [
- "babel",
- "cleo",
- "dateutil",
- "freezegun",
- "pytzdata",
-]
-
-
-[build-system]
-requires = ["poetry-core>=1.0.0a9"]
-build-backend = "poetry.core.masonry.api"
+[tool.poetry]
+name = "pendulum"
+version = "3.0.0a1"
+description = "Python datetimes made easy"
+authors = ["Sébastien Eustace <sebastien@eustace.io>"]
+license = "MIT"
+readme = 'README.rst'
+homepage = "https://pendulum.eustace.io"
+repository = "https://github.com/sdispater/pendulum"
+documentation = "https://pendulum.eustace.io/docs"
+keywords = ['datetime', 'date', 'time']
+
+packages = [
+ { include = "pendulum" },
+ { include = "tests", format = "sdist" },
+]
+include = [
+ { path = "meson.build", format = "sdist" },
+ { path = "pendulum/py.typed" },
+ # C extensions must be included in the wheel distributions
+ { path = "pendulum/_extensions/*.so", format = "wheel" },
+ { path = "pendulum/_extensions/*.pyd", format = "wheel" },
+ { path = "pendulum/parsing/*.so", format = "wheel" },
+ { path = "pendulum/parsing/*.pyd", format = "wheel" },
+]
+
+
+[tool.poetry.dependencies]
+python = "^3.7"
+python-dateutil = "^2.6"
+"backports.zoneinfo" = { version = "^0.2.1", python = ">=3.7,<3.9" }
+time-machine = { version = "^2.6.0", markers = "implementation_name != 'pypy'" }
+tzdata = ">=2020.1"
+importlib-resources = { version = "^5.9.0", python = ">=3.7,<3.9" }
+
+[tool.poetry.group.test.dependencies]
+pytest = "^7.1.2"
+pytest-cov = "^3.0.0"
+pytz = ">=2022.1"
+time-machine = "^2.7.1"
+
+[tool.poetry.group.doc.dependencies]
+mkdocs = "^1.0"
+pymdown-extensions = "^6.0"
+pygments = "^2.2"
+markdown-include = "^0.5.1"
+
+[tool.poetry.group.lint.dependencies]
+black = { version = "^22.6.0", markers = "implementation_name != 'pypy'" }
+isort = "^5.10.1"
+pre-commit = "^2.20.0"
+types-backports = "^0.1.3"
+types-python-dateutil = "^2.8.19"
+
+[tool.poetry.group.dev.dependencies]
+babel = "^2.10.3"
+cleo = "^1.0.0a5"
+tox = "^3.25.1"
+
+[tool.poetry.group.build]
+optional = true
+
+[tool.poetry.group.build.dependencies]
+meson = "^0.63.2"
+ninja = "^1.10.2.3"
+
+[tool.poetry.build]
+generate-setup-file = false
+script = "build.py"
+
+[tool.isort]
+profile = "black"
+force_single_line = true
+atomic = true
+lines_after_imports = -1
+lines_between_types = 1
+skip_glob = [
+ "pendulum/locales/**",
+ "build.py",
+ "pendulum/__version__.py",
+]
+filter_files = true
+known_first_party = "pendulum"
+known_third_party = [
+ "babel",
+ "cleo",
+ "dateutil",
+ "time_machine",
+ "pytzdata",
+]
+
+[tool.mypy]
+strict = true
+files = "pendulum, tests"
+show_error_codes = true
+pretty = true
+
+# The following whitelist is used to allow for incremental adoption
+# of Mypy. Modules should be removed from this whitelist as and when
+# their respective type errors have been addressed. No new modules
+# should be added to this whitelist.
+
+[[tool.mypy.overrides]]
+module = [
+ "pendulum.mixins.default",
+ "tests.conftest",
+ "tests.test_helpers",
+ "tests.test_main",
+ "tests.test_parsing",
+ "tests.date.test_add",
+ "tests.date.test_behavior",
+ "tests.date.test_construct",
+ "tests.date.test_comparison",
+ "tests.date.test_day_of_week_modifiers",
+ "tests.date.test_diff",
+ "tests.date.test_fluent_setters",
+ "tests.date.test_getters",
+ "tests.date.test_start_end_of",
+ "tests.date.test_strings",
+ "tests.date.test_sub",
+ "tests.datetime.test_add",
+ "tests.datetime.test_behavior",
+ "tests.datetime.test_construct",
+ "tests.datetime.test_comparison",
+ "tests.datetime.test_create_from_timestamp",
+ "tests.datetime.test_day_of_week_modifiers",
+ "tests.datetime.test_diff",
+ "tests.datetime.test_fluent_setters",
+ "tests.datetime.test_from_format",
+ "tests.datetime.test_getters",
+ "tests.datetime.test_naive",
+ "tests.datetime.test_replace",
+ "tests.datetime.test_start_end_of",
+ "tests.datetime.test_strings",
+ "tests.datetime.test_sub",
+ "tests.datetime.test_timezone",
+ "tests.duration.test_add_sub",
+ "tests.duration.test_arithmetic",
+ "tests.duration.test_behavior",
+ "tests.duration.test_construct",
+ "tests.duration.test_in_methods",
+ "tests.duration.test_in_words",
+ "tests.duration.test_total_methods",
+ "tests.formatting.test_formatter",
+ "tests.helpers.test_local_time",
+ "tests.localization.*",
+ "tests.parsing.test_parsing",
+ "tests.parsing.test_parsing_duration",
+ "tests.parsing.test_parse_iso8601",
+ "tests.interval.test_add_subtract",
+ "tests.interval.test_arithmetic",
+ "tests.interval.test_behavior",
+ "tests.interval.test_construct",
+ "tests.interval.test_hashing",
+ "tests.interval.test_in_words",
+ "tests.interval.test_range",
+ "tests.time.test_add",
+ "tests.time.test_behavior",
+ "tests.time.test_comparison",
+ "tests.time.test_construct",
+ "tests.time.test_diff",
+ "tests.time.test_fluent_setters",
+ "tests.time.test_strings",
+ "tests.time.test_sub",
+ "tests.tz.test_helpers",
+ "tests.tz.test_local_timezone",
+ "tests.tz.test_timezone",
+ "tests.tz.test_timezones",
+]
+ignore_errors = true
+
+[tool.coverage.run]
+omit = [
+ "pendulum/locales/*",
+ "pendulum/__version__.py,",
+ "pendulum/_extensions/*",
+ "pendulum/parsing/iso8601.py",
+ "pendulum/utils/_compat.py",
+]
+
+[build-system]
+requires = ["poetry-core>=1.1.0a6", "meson", "ninja"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..060e951
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,101 @@
+from __future__ import annotations
+
+import pytest
+
+import pendulum
+
+
+@pytest.fixture(autouse=True)
+def setup():
+ pendulum.set_local_timezone(pendulum.timezone("America/Toronto"))
+
+ yield
+
+ pendulum.set_locale("en")
+ pendulum.set_local_timezone()
+ pendulum.week_starts_at(pendulum.MONDAY)
+ pendulum.week_ends_at(pendulum.SUNDAY)
+
+
+def assert_datetime(
+ d, year, month, day, hour=None, minute=None, second=None, microsecond=None
+):
+ assert year == d.year
+ assert month == d.month
+ assert day == d.day
+
+ if hour is not None:
+ assert hour == d.hour
+
+ if minute is not None:
+ assert minute == d.minute
+
+ if second is not None:
+ assert second == d.second
+
+ if microsecond is not None:
+ assert microsecond == d.microsecond
+
+
+def assert_date(d, year, month, day):
+ assert year == d.year
+ assert month == d.month
+ assert day == d.day
+
+
+def assert_time(t, hour, minute, second, microsecond=None):
+ assert hour == t.hour
+ assert minute == t.minute
+ assert second == t.second
+
+ if microsecond is not None:
+ assert microsecond == t.microsecond
+
+
+def assert_duration(
+ dur,
+ years=None,
+ months=None,
+ weeks=None,
+ days=None,
+ hours=None,
+ minutes=None,
+ seconds=None,
+ microseconds=None,
+) -> None:
+ expected = {}
+ actual = {}
+
+ if years is not None:
+ expected["years"] = dur.years
+ actual["years"] = years
+
+ if months is not None:
+ expected["months"] = dur.months
+ actual["months"] = months
+
+ if weeks is not None:
+ expected["weeks"] = dur.weeks
+ actual["weeks"] = weeks
+
+ if days is not None:
+ expected["days"] = dur.remaining_days
+ actual["days"] = days
+
+ if hours is not None:
+ expected["hours"] = dur.hours
+ actual["hours"] = hours
+
+ if minutes is not None:
+ expected["minutes"] = dur.minutes
+ actual["minutes"] = minutes
+
+ if seconds is not None:
+ expected["seconds"] = dur.remaining_seconds
+ actual["seconds"] = seconds
+
+ if microseconds is not None:
+ expected["microseconds"] = dur.microseconds
+ actual["microseconds"] = microseconds
+
+ assert expected == actual
diff --git a/tests/date/__init__.py b/tests/date/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/date/__init__.py
diff --git a/tests/date/test_add.py b/tests/date/test_add.py
new file mode 100644
index 0000000..a435f46
--- /dev/null
+++ b/tests/date/test_add.py
@@ -0,0 +1,88 @@
+from __future__ import annotations
+
+from datetime import timedelta
+
+import pytest
+
+import pendulum
+
+from tests.conftest import assert_date
+
+
+def test_add_years_positive():
+ assert pendulum.date(1975, 1, 1).add(years=1).year == 1976
+
+
+def test_add_years_zero():
+ assert pendulum.date(1975, 1, 1).add(years=0).year == 1975
+
+
+def test_add_years_negative():
+ assert pendulum.date(1975, 1, 1).add(years=-1).year == 1974
+
+
+def test_add_months_positive():
+ assert pendulum.date(1975, 12, 1).add(months=1).month == 1
+
+
+def test_add_months_zero():
+ assert pendulum.date(1975, 12, 1).add(months=0).month == 12
+
+
+def test_add_months_negative():
+ assert pendulum.date(1975, 12, 1).add(months=-1).month == 11
+
+
+def test_add_month_with_overflow():
+ assert pendulum.Date(2012, 1, 31).add(months=1).month == 2
+
+
+def test_add_days_positive():
+ assert pendulum.Date(1975, 5, 31).add(days=1).day == 1
+
+
+def test_add_days_zero():
+ assert pendulum.Date(1975, 5, 31).add(days=0).day == 31
+
+
+def test_add_days_negative():
+ assert pendulum.Date(1975, 5, 31).add(days=-1).day == 30
+
+
+def test_add_weeks_positive():
+ assert pendulum.Date(1975, 5, 21).add(weeks=1).day == 28
+
+
+def test_add_weeks_zero():
+ assert pendulum.Date(1975, 5, 21).add(weeks=0).day == 21
+
+
+def test_add_weeks_negative():
+ assert pendulum.Date(1975, 5, 21).add(weeks=-1).day == 14
+
+
+def test_add_timedelta():
+ delta = timedelta(days=18)
+ d = pendulum.date(2015, 3, 14)
+
+ new = d + delta
+ assert isinstance(new, pendulum.Date)
+ assert_date(new, 2015, 4, 1)
+
+
+def test_add_duration():
+ duration = pendulum.duration(years=2, months=3, days=18)
+ d = pendulum.Date(2015, 3, 14)
+
+ new = d + duration
+ assert_date(new, 2017, 7, 2)
+
+
+def test_addition_invalid_type():
+ d = pendulum.date(2015, 3, 14)
+
+ with pytest.raises(TypeError):
+ d + 3
+
+ with pytest.raises(TypeError):
+ 3 + d
diff --git a/tests/date/test_behavior.py b/tests/date/test_behavior.py
new file mode 100644
index 0000000..98dd175
--- /dev/null
+++ b/tests/date/test_behavior.py
@@ -0,0 +1,73 @@
+from __future__ import annotations
+
+import pickle
+
+from datetime import date
+
+import pytest
+
+import pendulum
+
+
+@pytest.fixture()
+def p():
+ return pendulum.Date(2016, 8, 27)
+
+
+@pytest.fixture()
+def d():
+ return date(2016, 8, 27)
+
+
+def test_timetuple(p, d):
+ assert p.timetuple() == d.timetuple()
+
+
+def test_ctime(p, d):
+ assert p.ctime() == d.ctime()
+
+
+def test_isoformat(p, d):
+ assert p.isoformat() == d.isoformat()
+
+
+def test_toordinal(p, d):
+ assert p.toordinal() == d.toordinal()
+
+
+def test_weekday(p, d):
+ assert p.weekday() == d.weekday()
+
+
+def test_isoweekday(p, d):
+ assert p.isoweekday() == d.isoweekday()
+
+
+def test_isocalendar(p, d):
+ assert p.isocalendar() == d.isocalendar()
+
+
+def test_fromtimestamp():
+ assert pendulum.Date.fromtimestamp(0) == date.fromtimestamp(0)
+
+
+def test_fromordinal():
+ assert pendulum.Date.fromordinal(730120) == date.fromordinal(730120)
+
+
+def test_hash():
+ d1 = pendulum.Date(2016, 8, 27)
+ d2 = pendulum.Date(2016, 8, 27)
+ d3 = pendulum.Date(2016, 8, 28)
+
+ assert hash(d2) == hash(d1)
+ assert hash(d1) != hash(d3)
+
+
+def test_pickle():
+ d1 = pendulum.Date(2016, 8, 27)
+ s = pickle.dumps(d1)
+ d2 = pickle.loads(s)
+
+ assert isinstance(d2, pendulum.Date)
+ assert d2 == d1
diff --git a/tests/date/test_comparison.py b/tests/date/test_comparison.py
new file mode 100644
index 0000000..94bf224
--- /dev/null
+++ b/tests/date/test_comparison.py
@@ -0,0 +1,247 @@
+from __future__ import annotations
+
+from datetime import date
+
+import pendulum
+
+from tests.conftest import assert_date
+
+
+def test_equal_to_true():
+ d1 = pendulum.Date(2000, 1, 1)
+ d2 = pendulum.Date(2000, 1, 1)
+ d3 = date(2000, 1, 1)
+
+ assert d2 == d1
+ assert d3 == d1
+
+
+def test_equal_to_false():
+ d1 = pendulum.Date(2000, 1, 1)
+ d2 = pendulum.Date(2000, 1, 2)
+ d3 = date(2000, 1, 2)
+
+ assert d1 != d2
+ assert d1 != d3
+
+
+def test_not_equal_to_true():
+ d1 = pendulum.Date(2000, 1, 1)
+ d2 = pendulum.Date(2000, 1, 2)
+ d3 = date(2000, 1, 2)
+
+ assert d1 != d2
+ assert d1 != d3
+
+
+def test_not_equal_to_false():
+ d1 = pendulum.Date(2000, 1, 1)
+ d2 = pendulum.Date(2000, 1, 1)
+ d3 = date(2000, 1, 1)
+
+ assert d2 == d1
+ assert d3 == d1
+
+
+def test_not_equal_to_none():
+ d1 = pendulum.Date(2000, 1, 1)
+
+ assert d1 != None # noqa
+
+
+def test_greater_than_true():
+ d1 = pendulum.Date(2000, 1, 1)
+ d2 = pendulum.Date(1999, 12, 31)
+ d3 = date(1999, 12, 31)
+
+ assert d1 > d2
+ assert d1 > d3
+
+
+def test_greater_than_false():
+ d1 = pendulum.Date(2000, 1, 1)
+ d2 = pendulum.Date(2000, 1, 2)
+ d3 = date(2000, 1, 2)
+
+ assert not d1 > d2
+ assert not d1 > d3
+
+
+def test_greater_than_or_equal_true():
+ d1 = pendulum.Date(2000, 1, 1)
+ d2 = pendulum.Date(1999, 12, 31)
+ d3 = date(1999, 12, 31)
+
+ assert d1 >= d2
+ assert d1 >= d3
+
+
+def test_greater_than_or_equal_true_equal():
+ d1 = pendulum.Date(2000, 1, 1)
+ d2 = pendulum.Date(2000, 1, 1)
+ d3 = date(2000, 1, 1)
+
+ assert d1 >= d2
+ assert d1 >= d3
+
+
+def test_greater_than_or_equal_false():
+ d1 = pendulum.Date(2000, 1, 1)
+ d2 = pendulum.Date(2000, 1, 2)
+ d3 = date(2000, 1, 2)
+
+ assert not d1 >= d2
+ assert not d1 >= d3
+
+
+def test_less_than_true():
+ d1 = pendulum.Date(2000, 1, 1)
+ d2 = pendulum.Date(2000, 1, 2)
+ d3 = date(2000, 1, 2)
+
+ assert d1 < d2
+ assert d1 < d3
+
+
+def test_less_than_false():
+ d1 = pendulum.Date(2000, 1, 2)
+ d2 = pendulum.Date(2000, 1, 1)
+ d3 = date(2000, 1, 1)
+
+ assert not d1 < d2
+ assert not d1 < d3
+
+
+def test_less_than_or_equal_true():
+ d1 = pendulum.Date(2000, 1, 1)
+ d2 = pendulum.Date(2000, 1, 2)
+ d3 = date(2000, 1, 2)
+
+ assert d1 <= d2
+ assert d1 <= d3
+
+
+def test_less_than_or_equal_true_equal():
+ d1 = pendulum.Date(2000, 1, 1)
+ d2 = pendulum.Date(2000, 1, 1)
+ d3 = date(2000, 1, 1)
+
+ assert d1 <= d2
+ assert d1 <= d3
+
+
+def test_less_than_or_equal_false():
+ d1 = pendulum.Date(2000, 1, 2)
+ d2 = pendulum.Date(2000, 1, 1)
+ d3 = date(2000, 1, 1)
+
+ assert not d1 <= d2
+ assert not d1 <= d3
+
+
+def test_is_anniversary():
+ d = pendulum.Date.today()
+ an_anniversary = d.subtract(years=1)
+ assert an_anniversary.is_anniversary()
+ not_an_anniversary = d.subtract(days=1)
+ assert not not_an_anniversary.is_anniversary()
+ also_not_an_anniversary = d.add(days=2)
+ assert not also_not_an_anniversary.is_anniversary()
+
+ d1 = pendulum.Date(1987, 4, 23)
+ d2 = pendulum.Date(2014, 9, 26)
+ d3 = pendulum.Date(2014, 4, 23)
+ assert not d2.is_anniversary(d1)
+ assert d3.is_anniversary(d1)
+
+
+def test_is_birthday(): # backward compatibility
+ d = pendulum.Date.today()
+ an_anniversary = d.subtract(years=1)
+ assert an_anniversary.is_birthday()
+ not_an_anniversary = d.subtract(days=1)
+ assert not not_an_anniversary.is_birthday()
+ also_not_an_anniversary = d.add(days=2)
+ assert not also_not_an_anniversary.is_birthday()
+
+ d1 = pendulum.Date(1987, 4, 23)
+ d2 = pendulum.Date(2014, 9, 26)
+ d3 = pendulum.Date(2014, 4, 23)
+ assert not d2.is_birthday(d1)
+ assert d3.is_birthday(d1)
+
+
+def test_closest():
+ instance = pendulum.Date(2015, 5, 28)
+ dt1 = pendulum.Date(2015, 5, 27)
+ dt2 = pendulum.Date(2015, 5, 30)
+ closest = instance.closest(dt1, dt2)
+ assert closest == dt1
+
+ closest = instance.closest(dt2, dt1)
+ assert closest == dt1
+
+
+def test_closest_with_date():
+ instance = pendulum.Date(2015, 5, 28)
+ dt1 = date(2015, 5, 27)
+ dt2 = date(2015, 5, 30)
+ closest = instance.closest(dt1, dt2)
+ assert isinstance(closest, pendulum.Date)
+ assert_date(closest, 2015, 5, 27)
+
+
+def test_closest_with_equals():
+ instance = pendulum.Date(2015, 5, 28)
+ dt1 = pendulum.Date(2015, 5, 28)
+ dt2 = pendulum.Date(2015, 5, 30)
+ closest = instance.closest(dt1, dt2)
+ assert closest == dt1
+
+
+def test_farthest():
+ instance = pendulum.Date(2015, 5, 28)
+ dt1 = pendulum.Date(2015, 5, 27)
+ dt2 = pendulum.Date(2015, 5, 30)
+ closest = instance.farthest(dt1, dt2)
+ assert closest == dt2
+
+ closest = instance.farthest(dt2, dt1)
+ assert closest == dt2
+
+
+def test_farthest_with_date():
+ instance = pendulum.Date(2015, 5, 28)
+ dt1 = date(2015, 5, 27)
+ dt2 = date(2015, 5, 30)
+ closest = instance.farthest(dt1, dt2)
+ assert isinstance(closest, pendulum.Date)
+ assert_date(closest, 2015, 5, 30)
+
+
+def test_farthest_with_equals():
+ instance = pendulum.Date(2015, 5, 28)
+ dt1 = pendulum.Date(2015, 5, 28)
+ dt2 = pendulum.Date(2015, 5, 30)
+ closest = instance.farthest(dt1, dt2)
+ assert closest == dt2
+
+
+def test_is_same_day():
+ dt1 = pendulum.Date(2015, 5, 28)
+ dt2 = pendulum.Date(2015, 5, 29)
+ dt3 = pendulum.Date(2015, 5, 28)
+ dt4 = date(2015, 5, 28)
+ dt5 = date(2015, 5, 29)
+
+ assert not dt1.is_same_day(dt2)
+ assert dt1.is_same_day(dt3)
+ assert dt1.is_same_day(dt4)
+ assert not dt1.is_same_day(dt5)
+
+
+def test_comparison_to_unsupported():
+ dt1 = pendulum.Date.today()
+
+ assert dt1 != "test"
+ assert dt1 not in ["test"]
diff --git a/tests/date/test_construct.py b/tests/date/test_construct.py
new file mode 100644
index 0000000..5b6eb78
--- /dev/null
+++ b/tests/date/test_construct.py
@@ -0,0 +1,16 @@
+from __future__ import annotations
+
+from pendulum import Date
+from tests.conftest import assert_date
+
+
+def test_construct():
+ d = Date(2016, 10, 20)
+
+ assert_date(d, 2016, 10, 20)
+
+
+def test_today():
+ d = Date.today()
+
+ assert isinstance(d, Date)
diff --git a/tests/date/test_day_of_week_modifiers.py b/tests/date/test_day_of_week_modifiers.py
new file mode 100644
index 0000000..55aba55
--- /dev/null
+++ b/tests/date/test_day_of_week_modifiers.py
@@ -0,0 +1,298 @@
+from __future__ import annotations
+
+import pytest
+
+import pendulum
+
+from pendulum.exceptions import PendulumException
+from tests.conftest import assert_date
+
+
+def test_start_of_week():
+ d = pendulum.date(1980, 8, 7).start_of("week")
+ assert_date(d, 1980, 8, 4)
+
+
+def test_start_of_week_from_week_start():
+ d = pendulum.date(1980, 8, 4).start_of("week")
+ assert_date(d, 1980, 8, 4)
+
+
+def test_start_of_week_crossing_year_boundary():
+ d = pendulum.date(2014, 1, 1).start_of("week")
+ assert_date(d, 2013, 12, 30)
+
+
+def test_end_of_week():
+ d = pendulum.date(1980, 8, 7).end_of("week")
+ assert_date(d, 1980, 8, 10)
+
+
+def test_end_of_week_from_week_end():
+ d = pendulum.date(1980, 8, 10).end_of("week")
+ assert_date(d, 1980, 8, 10)
+
+
+def test_end_of_week_crossing_year_boundary():
+ d = pendulum.date(2013, 12, 31).end_of("week")
+ assert_date(d, 2014, 1, 5)
+
+
+def test_next():
+ d = pendulum.date(1975, 5, 21).next()
+ assert_date(d, 1975, 5, 28)
+
+
+def test_next_monday():
+ d = pendulum.date(1975, 5, 21).next(pendulum.MONDAY)
+ assert_date(d, 1975, 5, 26)
+
+
+def test_next_saturday():
+ d = pendulum.date(1975, 5, 21).next(6)
+ assert_date(d, 1975, 5, 24)
+
+
+def test_next_invalid():
+ dt = pendulum.date(1975, 5, 21)
+
+ with pytest.raises(ValueError):
+ dt.next(7)
+
+
+def test_previous():
+ d = pendulum.date(1975, 5, 21).previous()
+ assert_date(d, 1975, 5, 14)
+
+
+def test_previous_monday():
+ d = pendulum.date(1975, 5, 21).previous(pendulum.MONDAY)
+ assert_date(d, 1975, 5, 19)
+
+
+def test_previous_saturday():
+ d = pendulum.date(1975, 5, 21).previous(6)
+ assert_date(d, 1975, 5, 17)
+
+
+def test_previous_invalid():
+ dt = pendulum.date(1975, 5, 21)
+
+ with pytest.raises(ValueError):
+ dt.previous(7)
+
+
+def test_first_day_of_month():
+ d = pendulum.date(1975, 11, 21).first_of("month")
+ assert_date(d, 1975, 11, 1)
+
+
+def test_first_wednesday_of_month():
+ d = pendulum.date(1975, 11, 21).first_of("month", pendulum.WEDNESDAY)
+ assert_date(d, 1975, 11, 5)
+
+
+def test_first_friday_of_month():
+ d = pendulum.date(1975, 11, 21).first_of("month", 5)
+ assert_date(d, 1975, 11, 7)
+
+
+def test_last_day_of_month():
+ d = pendulum.date(1975, 12, 5).last_of("month")
+ assert_date(d, 1975, 12, 31)
+
+
+def test_last_tuesday_of_month():
+ d = pendulum.date(1975, 12, 1).last_of("month", pendulum.TUESDAY)
+ assert_date(d, 1975, 12, 30)
+
+
+def test_last_friday_of_month():
+ d = pendulum.date(1975, 12, 5).last_of("month", 5)
+ assert_date(d, 1975, 12, 26)
+
+
+def test_nth_of_month_outside_scope():
+ d = pendulum.date(1975, 6, 5)
+
+ with pytest.raises(PendulumException):
+ d.nth_of("month", 6, pendulum.MONDAY)
+
+
+def test_nth_of_month_outside_year():
+ d = pendulum.date(1975, 12, 5)
+
+ with pytest.raises(PendulumException):
+ d.nth_of("month", 55, pendulum.MONDAY)
+
+
+def test_nth_of_month_first():
+ d = pendulum.date(1975, 12, 5).nth_of("month", 1, pendulum.MONDAY)
+
+ assert_date(d, 1975, 12, 1)
+
+
+def test_2nd_monday_of_month():
+ d = pendulum.date(1975, 12, 5).nth_of("month", 2, pendulum.MONDAY)
+
+ assert_date(d, 1975, 12, 8)
+
+
+def test_3rd_wednesday_of_month():
+ d = pendulum.date(1975, 12, 5).nth_of("month", 3, 3)
+
+ assert_date(d, 1975, 12, 17)
+
+
+def test_first_day_of_quarter():
+ d = pendulum.date(1975, 11, 21).first_of("quarter")
+ assert_date(d, 1975, 10, 1)
+
+
+def test_first_wednesday_of_quarter():
+ d = pendulum.date(1975, 11, 21).first_of("quarter", pendulum.WEDNESDAY)
+ assert_date(d, 1975, 10, 1)
+
+
+def test_first_friday_of_quarter():
+ d = pendulum.date(1975, 11, 21).first_of("quarter", 5)
+ assert_date(d, 1975, 10, 3)
+
+
+def test_first_of_quarter_from_a_day_that_will_not_exist_in_the_first_month():
+ d = pendulum.date(2014, 5, 31).first_of("quarter")
+ assert_date(d, 2014, 4, 1)
+
+
+def test_last_day_of_quarter():
+ d = pendulum.date(1975, 8, 5).last_of("quarter")
+ assert_date(d, 1975, 9, 30)
+
+
+def test_last_tuesday_of_quarter():
+ d = pendulum.date(1975, 8, 5).last_of("quarter", pendulum.TUESDAY)
+ assert_date(d, 1975, 9, 30)
+
+
+def test_last_friday_of_quarter():
+ d = pendulum.date(1975, 8, 5).last_of("quarter", pendulum.FRIDAY)
+ assert_date(d, 1975, 9, 26)
+
+
+def test_last_day_of_quarter_that_will_not_exist_in_the_last_month():
+ d = pendulum.date(2014, 5, 31).last_of("quarter")
+ assert_date(d, 2014, 6, 30)
+
+
+def test_nth_of_quarter_outside_scope():
+ d = pendulum.date(1975, 1, 5)
+
+ with pytest.raises(PendulumException):
+ d.nth_of("quarter", 20, pendulum.MONDAY)
+
+
+def test_nth_of_quarter_outside_year():
+ d = pendulum.date(1975, 1, 5)
+
+ with pytest.raises(PendulumException):
+ d.nth_of("quarter", 55, pendulum.MONDAY)
+
+
+def test_nth_of_quarter_first():
+ d = pendulum.date(1975, 12, 5).nth_of("quarter", 1, pendulum.MONDAY)
+
+ assert_date(d, 1975, 10, 6)
+
+
+def test_nth_of_quarter_from_a_day_that_will_not_exist_in_the_first_month():
+ d = pendulum.date(2014, 5, 31).nth_of("quarter", 2, pendulum.MONDAY)
+ assert_date(d, 2014, 4, 14)
+
+
+def test_2nd_monday_of_quarter():
+ d = pendulum.date(1975, 8, 5).nth_of("quarter", 2, pendulum.MONDAY)
+ assert_date(d, 1975, 7, 14)
+
+
+def test_3rd_wednesday_of_quarter():
+ d = pendulum.date(1975, 8, 5).nth_of("quarter", 3, 3)
+ assert_date(d, 1975, 7, 16)
+
+
+def test_first_day_of_year():
+ d = pendulum.date(1975, 11, 21).first_of("year")
+ assert_date(d, 1975, 1, 1)
+
+
+def test_first_wednesday_of_year():
+ d = pendulum.date(1975, 11, 21).first_of("year", pendulum.WEDNESDAY)
+ assert_date(d, 1975, 1, 1)
+
+
+def test_first_friday_of_year():
+ d = pendulum.date(1975, 11, 21).first_of("year", 5)
+ assert_date(d, 1975, 1, 3)
+
+
+def test_last_day_of_year():
+ d = pendulum.date(1975, 8, 5).last_of("year")
+ assert_date(d, 1975, 12, 31)
+
+
+def test_last_tuesday_of_year():
+ d = pendulum.date(1975, 8, 5).last_of("year", pendulum.TUESDAY)
+ assert_date(d, 1975, 12, 30)
+
+
+def test_last_friday_of_year():
+ d = pendulum.date(1975, 8, 5).last_of("year", 5)
+ assert_date(d, 1975, 12, 26)
+
+
+def test_nth_of_year_outside_scope():
+ d = pendulum.date(1975, 1, 5)
+
+ with pytest.raises(PendulumException):
+ d.nth_of("year", 55, pendulum.MONDAY)
+
+
+def test_nth_of_year_first():
+ d = pendulum.date(1975, 12, 5).nth_of("year", 1, pendulum.MONDAY)
+
+ assert_date(d, 1975, 1, 6)
+
+
+def test_2nd_monday_of_year():
+ d = pendulum.date(1975, 8, 5).nth_of("year", 2, pendulum.MONDAY)
+ assert_date(d, 1975, 1, 13)
+
+
+def test_2rd_wednesday_of_year():
+ d = pendulum.date(1975, 8, 5).nth_of("year", 3, pendulum.WEDNESDAY)
+ assert_date(d, 1975, 1, 15)
+
+
+def test_7th_thursday_of_year():
+ d = pendulum.date(1975, 8, 31).nth_of("year", 7, pendulum.THURSDAY)
+ assert_date(d, 1975, 2, 13)
+
+
+def test_first_of_invalid_unit():
+ d = pendulum.date(1975, 8, 5)
+
+ with pytest.raises(ValueError):
+ d.first_of("invalid", 3)
+
+
+def test_last_of_invalid_unit():
+ d = pendulum.date(1975, 8, 5)
+
+ with pytest.raises(ValueError):
+ d.last_of("invalid", 3)
+
+
+def test_nth_of_invalid_unit():
+ d = pendulum.date(1975, 8, 5)
+
+ with pytest.raises(ValueError):
+ d.nth_of("invalid", 3, pendulum.MONDAY)
diff --git a/tests/date/test_diff.py b/tests/date/test_diff.py
new file mode 100644
index 0000000..56358cc
--- /dev/null
+++ b/tests/date/test_diff.py
@@ -0,0 +1,365 @@
+from __future__ import annotations
+
+from datetime import date
+
+import pytest
+
+import pendulum
+
+
+@pytest.fixture
+def today():
+ return pendulum.today().date()
+
+
+def test_diff_in_years_positive():
+ dt = pendulum.date(2000, 1, 1)
+ assert dt.diff(dt.add(years=1)).in_years() == 1
+
+
+def test_diff_in_years_negative_with_sign():
+ dt = pendulum.date(2000, 1, 1)
+ assert dt.diff(dt.subtract(years=1), False).in_years() == -1
+
+
+def test_diff_in_years_negative_no_sign():
+ dt = pendulum.date(2000, 1, 1)
+ assert dt.diff(dt.subtract(years=1)).in_years() == 1
+
+
+def test_diff_in_years_vs_default_now(today):
+ assert today.subtract(years=1).diff().in_years() == 1
+
+
+def test_diff_in_years_ensure_is_truncated():
+ dt = pendulum.date(2000, 1, 1)
+ assert dt.diff(dt.add(years=1).add(months=7)).in_years() == 1
+
+
+def test_diff_in_months_positive():
+ dt = pendulum.date(2000, 1, 1)
+ assert dt.diff(dt.add(years=1).add(months=1)).in_months() == 13
+
+
+def test_diff_in_months_negative_with_sign():
+ dt = pendulum.date(2000, 1, 1)
+
+ assert dt.diff(dt.subtract(years=1).add(months=1), False).in_months() == -11
+
+
+def test_diff_in_months_negative_no_sign():
+ dt = pendulum.date(2000, 1, 1)
+ assert dt.diff(dt.subtract(years=1).add(months=1)).in_months() == 11
+
+
+def test_diff_in_months_vs_default_now(today):
+ assert today.subtract(years=1).diff().in_months() == 12
+
+
+def test_diff_in_months_ensure_is_truncated():
+ dt = pendulum.date(2000, 1, 1)
+ assert dt.diff(dt.add(months=1).add(days=16)).in_months() == 1
+
+
+def test_diff_in_days_positive():
+ dt = pendulum.date(2000, 1, 1)
+ assert dt.diff(dt.add(years=1)).in_days() == 366
+
+
+def test_diff_in_days_negative_with_sign():
+ dt = pendulum.date(2000, 1, 1)
+ assert dt.diff(dt.subtract(years=1), False).in_days() == -365
+
+
+def test_diff_in_days_negative_no_sign():
+ dt = pendulum.date(2000, 1, 1)
+ assert dt.diff(dt.subtract(years=1)).in_days() == 365
+
+
+def test_diff_in_days_vs_default_now(today):
+ assert today.subtract(weeks=1).diff().in_days() == 7
+
+
+def test_diff_in_weeks_positive():
+ dt = pendulum.date(2000, 1, 1)
+ assert dt.diff(dt.add(years=1)).in_weeks() == 52
+
+
+def test_diff_in_weeks_negative_with_sign():
+ dt = pendulum.date(2000, 1, 1)
+ assert dt.diff(dt.subtract(years=1), False).in_weeks() == -52
+
+
+def test_diff_in_weeks_negative_no_sign():
+ dt = pendulum.date(2000, 1, 1)
+ assert dt.diff(dt.subtract(years=1)).in_weeks() == 52
+
+
+def test_diff_in_weeks_vs_default_now(today):
+ assert today.subtract(weeks=1).diff().in_weeks() == 1
+
+
+def test_diff_in_weeks_ensure_is_truncated():
+ dt = pendulum.date(2000, 1, 1)
+ assert dt.diff(dt.add(weeks=1).subtract(days=1)).in_weeks() == 0
+
+
+def test_diff_for_humans_now_and_day(today):
+ assert today.subtract(days=1).diff_for_humans() == "1 day ago"
+
+
+def test_diff_for_humans_now_and_days(today):
+ assert today.subtract(days=2).diff_for_humans() == "2 days ago"
+
+
+def test_diff_for_humans_now_and_nearly_week(today):
+ assert today.subtract(days=6).diff_for_humans() == "6 days ago"
+
+
+def test_diff_for_humans_now_and_week(today):
+ assert today.subtract(weeks=1).diff_for_humans() == "1 week ago"
+
+
+def test_diff_for_humans_now_and_weeks(today):
+ assert today.subtract(weeks=2).diff_for_humans() == "2 weeks ago"
+
+
+def test_diff_for_humans_now_and_nearly_month(today):
+ assert today.subtract(weeks=3).diff_for_humans() == "3 weeks ago"
+
+
+def test_diff_for_humans_now_and_month():
+ with pendulum.travel_to(pendulum.datetime(2016, 4, 1)):
+ today = pendulum.today().date()
+
+ assert today.subtract(weeks=4).diff_for_humans() == "4 weeks ago"
+ assert today.subtract(months=1).diff_for_humans() == "1 month ago"
+
+ with pendulum.travel_to(pendulum.datetime(2017, 3, 1)):
+ today = pendulum.today().date()
+
+ assert today.subtract(weeks=4).diff_for_humans() == "1 month ago"
+
+
+def test_diff_for_humans_now_and_months(today):
+ assert today.subtract(months=2).diff_for_humans() == "2 months ago"
+
+
+def test_diff_for_humans_now_and_nearly_year(today):
+ assert today.subtract(months=11).diff_for_humans() == "11 months ago"
+
+
+def test_diff_for_humans_now_and_year(today):
+ assert today.subtract(years=1).diff_for_humans() == "1 year ago"
+
+
+def test_diff_for_humans_now_and_years(today):
+ assert today.subtract(years=2).diff_for_humans() == "2 years ago"
+
+
+def test_diff_for_humans_now_and_future_day(today):
+ assert today.add(days=1).diff_for_humans() == "in 1 day"
+
+
+def test_diff_for_humans_now_and_future_days(today):
+ assert today.add(days=2).diff_for_humans() == "in 2 days"
+
+
+def test_diff_for_humans_now_and_nearly_future_week(today):
+ assert today.add(days=6).diff_for_humans() == "in 6 days"
+
+
+def test_diff_for_humans_now_and_future_week(today):
+ assert today.add(weeks=1).diff_for_humans() == "in 1 week"
+
+
+def test_diff_for_humans_now_and_future_weeks(today):
+ assert today.add(weeks=2).diff_for_humans() == "in 2 weeks"
+
+
+def test_diff_for_humans_now_and_nearly_future_month(today):
+ assert today.add(weeks=3).diff_for_humans() == "in 3 weeks"
+
+
+def test_diff_for_humans_now_and_future_month():
+ with pendulum.travel_to(pendulum.datetime(2016, 3, 1)):
+ today = pendulum.today("UTC").date()
+
+ assert today.add(weeks=4).diff_for_humans() == "in 4 weeks"
+ assert today.add(months=1).diff_for_humans() == "in 1 month"
+
+ with pendulum.travel_to(pendulum.datetime(2017, 3, 31)):
+ today = pendulum.today("UTC").date()
+
+ assert today.add(months=1).diff_for_humans() == "in 1 month"
+
+ with pendulum.travel_to(pendulum.datetime(2017, 4, 30)):
+ today = pendulum.today("UTC").date()
+
+ assert today.add(months=1).diff_for_humans() == "in 1 month"
+
+ with pendulum.travel_to(pendulum.datetime(2017, 1, 31)):
+ today = pendulum.today("UTC").date()
+
+ assert today.add(weeks=4).diff_for_humans() == "in 1 month"
+
+
+def test_diff_for_humans_now_and_future_months(today):
+ assert today.add(months=2).diff_for_humans() == "in 2 months"
+
+
+def test_diff_for_humans_now_and_nearly_future_year(today):
+ assert today.add(months=11).diff_for_humans() == "in 11 months"
+
+
+def test_diff_for_humans_now_and_future_year(today):
+ assert today.add(years=1).diff_for_humans() == "in 1 year"
+
+
+def test_diff_for_humans_now_and_future_years(today):
+ assert today.add(years=2).diff_for_humans() == "in 2 years"
+
+
+def test_diff_for_humans_other_and_day(today):
+ assert today.diff_for_humans(today.add(days=1)) == "1 day before"
+
+
+def test_diff_for_humans_other_and_days(today):
+ assert today.diff_for_humans(today.add(days=2)) == "2 days before"
+
+
+def test_diff_for_humans_other_and_nearly_week(today):
+ assert today.diff_for_humans(today.add(days=6)) == "6 days before"
+
+
+def test_diff_for_humans_other_and_week(today):
+ assert today.diff_for_humans(today.add(weeks=1)) == "1 week before"
+
+
+def test_diff_for_humans_other_and_weeks(today):
+ assert today.diff_for_humans(today.add(weeks=2)) == "2 weeks before"
+
+
+def test_diff_for_humans_other_and_nearly_month(today):
+ assert today.diff_for_humans(today.add(weeks=3)) == "3 weeks before"
+
+
+def test_diff_for_humans_other_and_month():
+ with pendulum.travel_to(pendulum.datetime(2016, 3, 1)):
+ today = pendulum.today().date()
+
+ assert today.diff_for_humans(today.add(weeks=4)) == "4 weeks before"
+ assert today.diff_for_humans(today.add(months=1)) == "1 month before"
+
+ with pendulum.travel_to(pendulum.datetime(2017, 3, 31)):
+ today = pendulum.today().date()
+
+ assert today.diff_for_humans(today.add(months=1)) == "1 month before"
+
+ with pendulum.travel_to(pendulum.datetime(2017, 4, 30)):
+ today = pendulum.today().date()
+
+ assert today.diff_for_humans(today.add(months=1)) == "1 month before"
+
+ with pendulum.travel_to(pendulum.datetime(2017, 1, 31)):
+ today = pendulum.today().date()
+
+ assert today.diff_for_humans(today.add(weeks=4)) == "1 month before"
+
+
+def test_diff_for_humans_other_and_months(today):
+ assert today.diff_for_humans(today.add(months=2)) == "2 months before"
+
+
+def test_diff_for_humans_other_and_nearly_year(today):
+ assert today.diff_for_humans(today.add(months=11)) == "11 months before"
+
+
+def test_diff_for_humans_other_and_year(today):
+ assert today.diff_for_humans(today.add(years=1)) == "1 year before"
+
+
+def test_diff_for_humans_other_and_years(today):
+ assert today.diff_for_humans(today.add(years=2)) == "2 years before"
+
+
+def test_diff_for_humans_other_and_future_day(today):
+ assert today.diff_for_humans(today.subtract(days=1)) == "1 day after"
+
+
+def test_diff_for_humans_other_and_future_days(today):
+ assert today.diff_for_humans(today.subtract(days=2)) == "2 days after"
+
+
+def test_diff_for_humans_other_and_nearly_future_week(today):
+ assert today.diff_for_humans(today.subtract(days=6)) == "6 days after"
+
+
+def test_diff_for_humans_other_and_future_week(today):
+ assert today.diff_for_humans(today.subtract(weeks=1)) == "1 week after"
+
+
+def test_diff_for_humans_other_and_future_weeks(today):
+ assert today.diff_for_humans(today.subtract(weeks=2)) == "2 weeks after"
+
+
+def test_diff_for_humans_other_and_nearly_future_month(today):
+ assert today.diff_for_humans(today.subtract(weeks=3)) == "3 weeks after"
+
+
+def test_diff_for_humans_other_and_future_month():
+ with pendulum.travel_to(pendulum.datetime(2016, 3, 1)):
+ today = pendulum.today().date()
+
+ assert today.diff_for_humans(today.subtract(weeks=4)) == "4 weeks after"
+ assert today.diff_for_humans(today.subtract(months=1)) == "1 month after"
+
+ with pendulum.travel_to(pendulum.datetime(2017, 2, 28)):
+ today = pendulum.today().date()
+
+ assert today.diff_for_humans(today.subtract(weeks=4)) == "1 month after"
+
+
+def test_diff_for_humans_other_and_future_months(today):
+ assert today.diff_for_humans(today.subtract(months=2)) == "2 months after"
+
+
+def test_diff_for_humans_other_and_nearly_future_year(today):
+ assert today.diff_for_humans(today.subtract(months=11)) == "11 months after"
+
+
+def test_diff_for_humans_other_and_future_year(today):
+ assert today.diff_for_humans(today.subtract(years=1)) == "1 year after"
+
+
+def test_diff_for_humans_other_and_future_years(today):
+ assert today.diff_for_humans(today.subtract(years=2)) == "2 years after"
+
+
+def test_diff_for_humans_absolute_days(today):
+ assert today.diff_for_humans(today.subtract(days=2), True) == "2 days"
+ assert today.diff_for_humans(today.add(days=2), True) == "2 days"
+
+
+def test_diff_for_humans_absolute_weeks(today):
+ assert today.diff_for_humans(today.subtract(weeks=2), True) == "2 weeks"
+ assert today.diff_for_humans(today.add(weeks=2), True) == "2 weeks"
+
+
+def test_diff_for_humans_absolute_months(today):
+ assert today.diff_for_humans(today.subtract(months=2), True) == "2 months"
+ assert today.diff_for_humans(today.add(months=2), True) == "2 months"
+
+
+def test_diff_for_humans_absolute_years(today):
+ assert today.diff_for_humans(today.subtract(years=1), True) == "1 year"
+ assert today.diff_for_humans(today.add(years=1), True) == "1 year"
+
+
+def test_subtraction():
+ d = pendulum.date(2016, 7, 5)
+ future_dt = date(2016, 7, 6)
+ future = d.add(days=1)
+
+ assert (future - d).total_seconds() == 86400
+ assert (future_dt - d).total_seconds() == 86400
diff --git a/tests/date/test_fluent_setters.py b/tests/date/test_fluent_setters.py
new file mode 100644
index 0000000..c76cc2f
--- /dev/null
+++ b/tests/date/test_fluent_setters.py
@@ -0,0 +1,29 @@
+from __future__ import annotations
+
+import pendulum
+
+from tests.conftest import assert_date
+
+
+def test_fluid_year_setter():
+ d = pendulum.Date(2016, 10, 20)
+ new = d.set(year=1995)
+
+ assert_date(new, 1995, 10, 20)
+ assert new.year == 1995
+
+
+def test_fluid_month_setter():
+ d = pendulum.Date(2016, 7, 2)
+ new = d.set(month=11)
+
+ assert new.month == 11
+ assert d.month == 7
+
+
+def test_fluid_day_setter():
+ d = pendulum.Date(2016, 7, 2)
+ new = d.set(day=9)
+
+ assert new.day == 9
+ assert d.day == 2
diff --git a/tests/date/test_getters.py b/tests/date/test_getters.py
new file mode 100644
index 0000000..a492794
--- /dev/null
+++ b/tests/date/test_getters.py
@@ -0,0 +1,87 @@
+from __future__ import annotations
+
+import pendulum
+
+
+def test_year():
+ d = pendulum.Date(1234, 5, 6)
+ assert d.year == 1234
+
+
+def test_month():
+ d = pendulum.Date(1234, 5, 6)
+ assert d.month == 5
+
+
+def test_day():
+ d = pendulum.Date(1234, 5, 6)
+ assert d.day == 6
+
+
+def test_day_of_week():
+ d = pendulum.Date(2012, 5, 7)
+ assert d.day_of_week == pendulum.MONDAY
+
+
+def test_day_of_year():
+ d = pendulum.Date(2015, 12, 31)
+ assert d.day_of_year == 365
+ d = pendulum.Date(2016, 12, 31)
+ assert d.day_of_year == 366
+
+
+def test_days_in_month():
+ d = pendulum.Date(2012, 5, 7)
+ assert d.days_in_month == 31
+
+
+def test_age():
+ d = pendulum.Date.today()
+ assert d.age == 0
+ assert d.add(years=1).age == -1
+ assert d.subtract(years=1).age == 1
+
+
+def test_is_leap_year():
+ assert pendulum.Date(2012, 1, 1).is_leap_year()
+ assert not pendulum.Date(2011, 1, 1).is_leap_year()
+
+
+def test_is_long_year():
+ assert pendulum.Date(2015, 1, 1).is_long_year()
+ assert not pendulum.Date(2016, 1, 1).is_long_year()
+
+
+def test_week_of_month():
+ assert pendulum.Date(2012, 9, 30).week_of_month == 5
+ assert pendulum.Date(2012, 9, 28).week_of_month == 5
+ assert pendulum.Date(2012, 9, 20).week_of_month == 4
+ assert pendulum.Date(2012, 9, 8).week_of_month == 2
+ assert pendulum.Date(2012, 9, 1).week_of_month == 1
+ assert pendulum.date(2020, 1, 1).week_of_month == 1
+ assert pendulum.date(2020, 1, 7).week_of_month == 2
+ assert pendulum.date(2020, 1, 14).week_of_month == 3
+
+
+def test_week_of_year_first_week():
+ assert pendulum.Date(2012, 1, 1).week_of_year == 52
+ assert pendulum.Date(2012, 1, 2).week_of_year == 1
+
+
+def test_week_of_year_last_week():
+ assert pendulum.Date(2012, 12, 30).week_of_year == 52
+ assert pendulum.Date(2012, 12, 31).week_of_year == 1
+
+
+def test_is_future():
+ d = pendulum.Date.today()
+ assert not d.is_future()
+ d = d.add(days=1)
+ assert d.is_future()
+
+
+def test_is_past():
+ d = pendulum.Date.today()
+ assert not d.is_past()
+ d = d.subtract(days=1)
+ assert d.is_past()
diff --git a/tests/date/test_start_end_of.py b/tests/date/test_start_end_of.py
new file mode 100644
index 0000000..f1b4412
--- /dev/null
+++ b/tests/date/test_start_end_of.py
@@ -0,0 +1,252 @@
+from __future__ import annotations
+
+import pytest
+
+import pendulum
+
+from pendulum import Date
+from tests.conftest import assert_date
+
+
+def test_start_of_day():
+ d = Date.today()
+ new = d.start_of("day")
+ assert isinstance(new, Date)
+ assert_date(new, d.year, d.month, d.day)
+
+
+def test_end_of_day():
+ d = Date.today()
+ new = d.end_of("day")
+ assert isinstance(new, Date)
+ assert_date(new, d.year, d.month, d.day)
+
+
+def test_start_of_week():
+ d = Date(2016, 10, 20)
+ new = d.start_of("week")
+ assert isinstance(new, Date)
+ assert_date(new, d.year, d.month, 17)
+
+
+def test_end_of_week():
+ d = Date(2016, 10, 20)
+ new = d.end_of("week")
+ assert isinstance(new, Date)
+ assert_date(new, d.year, d.month, 23)
+
+
+def test_start_of_month_is_fluid():
+ d = Date.today()
+ assert isinstance(d.start_of("month"), Date)
+
+
+def test_start_of_month_from_now():
+ d = Date.today()
+ new = d.start_of("month")
+ assert_date(new, d.year, d.month, 1)
+
+
+def test_start_of_month_from_last_day():
+ d = Date(2000, 1, 31)
+ new = d.start_of("month")
+ assert_date(new, 2000, 1, 1)
+
+
+def test_start_of_year_is_fluid():
+ d = Date.today()
+ new = d.start_of("year")
+ assert isinstance(new, Date)
+
+
+def test_start_of_year_from_now():
+ d = Date.today()
+ new = d.start_of("year")
+ assert_date(new, d.year, 1, 1)
+
+
+def test_start_of_year_from_first_day():
+ d = Date(2000, 1, 1)
+ new = d.start_of("year")
+ assert_date(new, 2000, 1, 1)
+
+
+def test_start_of_year_from_last_day():
+ d = Date(2000, 12, 31)
+ new = d.start_of("year")
+ assert_date(new, 2000, 1, 1)
+
+
+def test_end_of_month_is_fluid():
+ d = Date.today()
+ assert isinstance(d.end_of("month"), Date)
+
+
+def test_end_of_month_from_now():
+ d = Date.today().start_of("month")
+ new = d.start_of("month")
+ assert_date(new, d.year, d.month, 1)
+
+
+def test_end_of_month():
+ d = Date(2000, 1, 1).end_of("month")
+ new = d.end_of("month")
+ assert_date(new, 2000, 1, 31)
+
+
+def test_end_of_month_from_last_day():
+ d = Date(2000, 1, 31)
+ new = d.end_of("month")
+ assert_date(new, 2000, 1, 31)
+
+
+def test_end_of_year_is_fluid():
+ d = Date.today()
+ assert isinstance(d.end_of("year"), Date)
+
+
+def test_end_of_year_from_now():
+ d = Date.today().end_of("year")
+ new = d.end_of("year")
+ assert_date(new, d.year, 12, 31)
+
+
+def test_end_of_year_from_first_day():
+ d = Date(2000, 1, 1)
+ new = d.end_of("year")
+ assert_date(new, 2000, 12, 31)
+
+
+def test_end_of_year_from_last_day():
+ d = Date(2000, 12, 31)
+ new = d.end_of("year")
+ assert_date(new, 2000, 12, 31)
+
+
+def test_start_of_decade_is_fluid():
+ d = Date.today()
+ assert isinstance(d.start_of("decade"), Date)
+
+
+def test_start_of_decade_from_now():
+ d = Date.today()
+ new = d.start_of("decade")
+ assert_date(new, d.year - d.year % 10, 1, 1)
+
+
+def test_start_of_decade_from_first_day():
+ d = Date(2000, 1, 1)
+ new = d.start_of("decade")
+ assert_date(new, 2000, 1, 1)
+
+
+def test_start_of_decade_from_last_day():
+ d = Date(2009, 12, 31)
+ new = d.start_of("decade")
+ assert_date(new, 2000, 1, 1)
+
+
+def test_end_of_decade_is_fluid():
+ d = Date.today()
+ assert isinstance(d.end_of("decade"), Date)
+
+
+def test_end_of_decade_from_now():
+ d = Date.today()
+ new = d.end_of("decade")
+ assert_date(new, d.year - d.year % 10 + 9, 12, 31)
+
+
+def test_end_of_decade_from_first_day():
+ d = Date(2000, 1, 1)
+ new = d.end_of("decade")
+ assert_date(new, 2009, 12, 31)
+
+
+def test_end_of_decade_from_last_day():
+ d = Date(2009, 12, 31)
+ new = d.end_of("decade")
+ assert_date(new, 2009, 12, 31)
+
+
+def test_start_of_century_is_fluid():
+ d = Date.today()
+ assert isinstance(d.start_of("century"), Date)
+
+
+def test_start_of_century_from_now():
+ d = Date.today()
+ new = d.start_of("century")
+ assert_date(new, d.year - d.year % 100 + 1, 1, 1)
+
+
+def test_start_of_century_from_first_day():
+ d = Date(2001, 1, 1)
+ new = d.start_of("century")
+ assert_date(new, 2001, 1, 1)
+
+
+def test_start_of_century_from_last_day():
+ d = Date(2100, 12, 31)
+ new = d.start_of("century")
+ assert_date(new, 2001, 1, 1)
+
+
+def test_end_of_century_is_fluid():
+ d = Date.today()
+ assert isinstance(d.end_of("century"), Date)
+
+
+def test_end_of_century_from_now():
+ now = Date.today()
+ d = now.end_of("century")
+ assert_date(d, now.year - now.year % 100 + 100, 12, 31)
+
+
+def test_end_of_century_from_first_day():
+ d = Date(2001, 1, 1)
+ new = d.end_of("century")
+ assert_date(new, 2100, 12, 31)
+
+
+def test_end_of_century_from_last_day():
+ d = Date(2100, 12, 31)
+ new = d.end_of("century")
+ assert_date(new, 2100, 12, 31)
+
+
+def test_average_is_fluid():
+ d = Date.today().average()
+ assert isinstance(d, Date)
+
+
+def test_average_from_same():
+ d1 = pendulum.date(2000, 1, 31)
+ d2 = pendulum.date(2000, 1, 31).average(d1)
+ assert_date(d2, 2000, 1, 31)
+
+
+def test_average_from_greater():
+ d1 = pendulum.date(2000, 1, 1)
+ d2 = pendulum.date(2009, 12, 31).average(d1)
+ assert_date(d2, 2004, 12, 31)
+
+
+def test_average_from_lower():
+ d1 = pendulum.date(2009, 12, 31)
+ d2 = pendulum.date(2000, 1, 1).average(d1)
+ assert_date(d2, 2004, 12, 31)
+
+
+def test_start_of():
+ d = pendulum.date(2013, 3, 31)
+
+ with pytest.raises(ValueError):
+ d.start_of("invalid")
+
+
+def test_end_of_invalid_unit():
+ d = pendulum.date(2013, 3, 31)
+
+ with pytest.raises(ValueError):
+ d.end_of("invalid")
diff --git a/tests/date/test_strings.py b/tests/date/test_strings.py
new file mode 100644
index 0000000..f373581
--- /dev/null
+++ b/tests/date/test_strings.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+import pendulum
+
+
+def test_to_string():
+ d = pendulum.Date(2016, 10, 16)
+ assert str(d) == "2016-10-16"
+
+
+def test_to_date_string():
+ d = pendulum.Date(1975, 12, 25)
+ assert d.to_date_string() == "1975-12-25"
+
+
+def test_to_formatted_date_string():
+ d = pendulum.Date(1975, 12, 25)
+ assert d.to_formatted_date_string() == "Dec 25, 1975"
+
+
+def test_repr():
+ d = pendulum.Date(1975, 12, 25)
+
+ assert repr(d) == "Date(1975, 12, 25)"
+ assert d.__repr__() == "Date(1975, 12, 25)"
+
+
+def test_format_with_locale():
+ d = pendulum.Date(1975, 12, 25)
+ expected = "jeudi 25e jour de décembre 1975"
+ assert d.format("dddd Do [jour de] MMMM YYYY", locale="fr") == expected
+
+
+def test_strftime():
+ d = pendulum.Date(1975, 12, 25)
+ assert d.strftime("%d") == "25"
+
+
+def test_for_json():
+ d = pendulum.Date(1975, 12, 25)
+ assert d.for_json() == "1975-12-25"
+
+
+def test_format():
+ d = pendulum.Date(1975, 12, 25)
+ assert f"{d}" == "1975-12-25"
+ assert f"{d:YYYY}" == "1975"
+ assert f"{d:%Y}" == "1975"
diff --git a/tests/date/test_sub.py b/tests/date/test_sub.py
new file mode 100644
index 0000000..33855a4
--- /dev/null
+++ b/tests/date/test_sub.py
@@ -0,0 +1,90 @@
+from __future__ import annotations
+
+from datetime import datetime
+from datetime import timedelta
+
+import pytest
+
+import pendulum
+
+from tests.conftest import assert_date
+
+
+def test_subtract_years_positive():
+ assert pendulum.date(1975, 1, 1).subtract(years=1).year == 1974
+
+
+def test_subtract_years_zero():
+ assert pendulum.date(1975, 1, 1).subtract(years=0).year == 1975
+
+
+def test_subtract_years_negative():
+ assert pendulum.date(1975, 1, 1).subtract(years=-1).year == 1976
+
+
+def test_subtract_months_positive():
+ assert pendulum.date(1975, 1, 1).subtract(months=1).month == 12
+
+
+def test_subtract_months_zero():
+ assert pendulum.date(1975, 12, 1).subtract(months=0).month == 12
+
+
+def test_subtract_months_negative():
+ assert pendulum.date(1975, 11, 1).subtract(months=-1).month == 12
+
+
+def test_subtract_days_positive():
+ assert pendulum.Date(1975, 6, 1).subtract(days=1).day == 31
+
+
+def test_subtract_days_zero():
+ assert pendulum.Date(1975, 5, 31).subtract(days=0).day == 31
+
+
+def test_subtract_days_negative():
+ assert pendulum.Date(1975, 5, 30).subtract(days=-1).day == 31
+
+
+def test_subtract_days_max():
+ delta = pendulum.now() - pendulum.instance(datetime.min)
+ assert pendulum.now().subtract(days=delta.days - 1).year == 1
+
+
+def test_subtract_weeks_positive():
+ assert pendulum.Date(1975, 5, 28).subtract(weeks=1).day == 21
+
+
+def test_subtract_weeks_zero():
+ assert pendulum.Date(1975, 5, 21).subtract(weeks=0).day == 21
+
+
+def test_subtract_weeks_negative():
+ assert pendulum.Date(1975, 5, 14).subtract(weeks=-1).day == 21
+
+
+def test_subtract_timedelta():
+ delta = timedelta(days=18)
+ d = pendulum.date(2015, 3, 14)
+
+ new = d - delta
+ assert isinstance(new, pendulum.Date)
+ assert_date(new, 2015, 2, 24)
+
+
+def test_subtract_duration():
+ delta = pendulum.duration(years=2, months=3, days=18)
+ d = pendulum.date(2015, 3, 14)
+
+ new = d - delta
+ assert_date(new, 2012, 11, 26)
+
+
+def test_addition_invalid_type():
+ d = pendulum.date(2015, 3, 14)
+
+ with pytest.raises(TypeError):
+ d - "ab"
+
+ with pytest.raises(TypeError):
+ "ab" - d
diff --git a/tests/datetime/__init__.py b/tests/datetime/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/datetime/__init__.py
diff --git a/tests/datetime/test_add.py b/tests/datetime/test_add.py
new file mode 100644
index 0000000..87ea39f
--- /dev/null
+++ b/tests/datetime/test_add.py
@@ -0,0 +1,268 @@
+from __future__ import annotations
+
+from datetime import timedelta
+
+import pytest
+
+import pendulum
+
+from tests.conftest import assert_datetime
+
+
+def test_add_years_positive():
+ assert pendulum.datetime(1975, 1, 1).add(years=1).year == 1976
+
+
+def test_add_years_zero():
+ assert pendulum.datetime(1975, 1, 1).add(years=0).year == 1975
+
+
+def test_add_years_negative():
+ assert pendulum.datetime(1975, 1, 1).add(years=-1).year == 1974
+
+
+def test_add_months_positive():
+ assert pendulum.datetime(1975, 12, 1).add(months=1).month == 1
+
+
+def test_add_months_zero():
+ assert pendulum.datetime(1975, 12, 1).add(months=0).month == 12
+
+
+def test_add_months_negative():
+ assert pendulum.datetime(1975, 12, 1).add(months=-1).month == 11
+
+
+def test_add_month_with_overflow():
+ assert pendulum.datetime(2012, 1, 31).add(months=1).month == 2
+
+
+def test_add_days_positive():
+ assert pendulum.datetime(1975, 5, 31).add(days=1).day == 1
+
+
+def test_add_days_zero():
+ assert pendulum.datetime(1975, 5, 31).add(days=0).day == 31
+
+
+def test_add_days_negative():
+ assert pendulum.datetime(1975, 5, 31).add(days=-1).day == 30
+
+
+def test_add_weeks_positive():
+ assert pendulum.datetime(1975, 5, 21).add(weeks=1).day == 28
+
+
+def test_add_weeks_zero():
+ assert pendulum.datetime(1975, 5, 21).add(weeks=0).day == 21
+
+
+def test_add_weeks_negative():
+ assert pendulum.datetime(1975, 5, 21).add(weeks=-1).day == 14
+
+
+def test_add_hours_positive():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).add(hours=1).hour == 1
+
+
+def test_add_hours_zero():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).add(hours=0).hour == 0
+
+
+def test_add_hours_negative():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).add(hours=-1).hour == 23
+
+
+def test_add_minutes_positive():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).add(minutes=1).minute == 1
+
+
+def test_add_minutes_zero():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).add(minutes=0).minute == 0
+
+
+def test_add_minutes_negative():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).add(minutes=-1).minute == 59
+
+
+def test_add_seconds_positive():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).add(seconds=1).second == 1
+
+
+def test_add_seconds_zero():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).add(seconds=0).second == 0
+
+
+def test_add_seconds_negative():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).add(seconds=-1).second == 59
+
+
+def test_add_timedelta():
+ delta = timedelta(days=6, seconds=45, microseconds=123456)
+ d = pendulum.datetime(2015, 3, 14, 3, 12, 15, 654321)
+
+ d = d + delta
+ assert d.day == 20
+ assert d.minute == 13
+ assert d.second == 0
+ assert d.microsecond == 777777
+
+
+def test_add_duration():
+ duration = pendulum.duration(
+ years=2, months=3, days=6, seconds=45, microseconds=123456
+ )
+ d = pendulum.datetime(2015, 3, 14, 3, 12, 15, 654321)
+
+ d = d + duration
+ assert d.year == 2017
+ assert d.month == 6
+ assert d.day == 20
+ assert d.hour == 3
+ assert d.minute == 13
+ assert d.second == 0
+ assert d.microsecond == 777777
+
+
+def test_addition_invalid_type():
+ d = pendulum.datetime(2015, 3, 14, 3, 12, 15, 654321)
+
+ with pytest.raises(TypeError):
+ d + 3
+
+ with pytest.raises(TypeError):
+ 3 + d
+
+
+def test_add_to_fixed_timezones():
+ dt = pendulum.parse("2015-03-08T01:00:00-06:00")
+ dt = dt.add(weeks=1)
+ dt = dt.add(hours=1)
+
+ assert_datetime(dt, 2015, 3, 15, 2, 0, 0)
+ assert dt.timezone_name == "-06:00"
+ assert dt.offset == -6 * 3600
+
+
+def test_add_time_to_new_transition_skipped():
+ dt = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, tz="Europe/Paris")
+
+ assert_datetime(dt, 2013, 3, 31, 1, 59, 59, 999999)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 3600
+ assert not dt.is_dst()
+
+ dt = dt.add(microseconds=1)
+
+ assert_datetime(dt, 2013, 3, 31, 3, 0, 0, 0)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 7200
+ assert dt.is_dst()
+
+ dt = pendulum.datetime(2013, 3, 10, 1, 59, 59, 999999, tz="America/New_York")
+
+ assert_datetime(dt, 2013, 3, 10, 1, 59, 59, 999999)
+ assert dt.timezone_name == "America/New_York"
+ assert dt.offset == -5 * 3600
+ assert not dt.is_dst()
+
+ dt = dt.add(microseconds=1)
+
+ assert_datetime(dt, 2013, 3, 10, 3, 0, 0, 0)
+ assert dt.timezone_name == "America/New_York"
+ assert dt.offset == -4 * 3600
+ assert dt.is_dst()
+
+ dt = pendulum.datetime(1957, 4, 28, 1, 59, 59, 999999, tz="America/New_York")
+
+ assert_datetime(dt, 1957, 4, 28, 1, 59, 59, 999999)
+ assert dt.timezone_name == "America/New_York"
+ assert dt.offset == -5 * 3600
+ assert not dt.is_dst()
+
+ dt = dt.add(microseconds=1)
+
+ assert_datetime(dt, 1957, 4, 28, 3, 0, 0, 0)
+ assert dt.timezone_name == "America/New_York"
+ assert dt.offset == -4 * 3600
+ assert dt.is_dst()
+
+
+def test_add_time_to_new_transition_skipped_big():
+ dt = pendulum.datetime(2013, 3, 31, 1, tz="Europe/Paris")
+
+ assert_datetime(dt, 2013, 3, 31, 1, 0, 0, 0)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 3600
+ assert not dt.is_dst()
+
+ dt = dt.add(weeks=1)
+
+ assert_datetime(dt, 2013, 4, 7, 1, 0, 0, 0)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 7200
+ assert dt.is_dst()
+
+
+def test_add_time_to_new_transition_repeated():
+ dt = pendulum.datetime(2013, 10, 27, 1, 59, 59, 999999, tz="Europe/Paris")
+ dt = dt.add(hours=1)
+
+ assert_datetime(dt, 2013, 10, 27, 2, 59, 59, 999999)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 7200
+ assert dt.is_dst()
+
+ dt = dt.add(microseconds=1)
+
+ assert_datetime(dt, 2013, 10, 27, 2, 0, 0, 0)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 3600
+ assert not dt.is_dst()
+
+ dt = pendulum.datetime(2013, 11, 3, 0, 59, 59, 999999, tz="America/New_York")
+ dt = dt.add(hours=1)
+
+ assert_datetime(dt, 2013, 11, 3, 1, 59, 59, 999999)
+ assert dt.timezone_name == "America/New_York"
+ assert dt.offset == -4 * 3600
+ assert dt.is_dst()
+
+ dt = dt.add(microseconds=1)
+
+ assert_datetime(dt, 2013, 11, 3, 1, 0, 0, 0)
+ assert dt.timezone_name == "America/New_York"
+ assert dt.offset == -5 * 3600
+ assert not dt.is_dst()
+
+
+def test_add_time_to_new_transition_repeated_big():
+ dt = pendulum.datetime(2013, 10, 27, 1, tz="Europe/Paris")
+
+ assert_datetime(dt, 2013, 10, 27, 1, 0, 0, 0)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 7200
+ assert dt.is_dst()
+
+ dt = dt.add(weeks=1)
+
+ assert_datetime(dt, 2013, 11, 3, 1, 0, 0, 0)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 3600
+ assert not dt.is_dst()
+
+
+def test_add_interval():
+ dt = pendulum.datetime(2017, 3, 11, 10, 45, tz="America/Los_Angeles")
+ new = dt + pendulum.duration(hours=24)
+
+ assert_datetime(new, 2017, 3, 12, 11, 45)
+
+
+def test_period_over_midnight_tz():
+ start = pendulum.datetime(2018, 2, 25, tz="Europe/Paris")
+ end = start.add(hours=1)
+ period = end - start
+ new_end = start + period
+
+ assert new_end == end
diff --git a/tests/datetime/test_behavior.py b/tests/datetime/test_behavior.py
new file mode 100644
index 0000000..e02323a
--- /dev/null
+++ b/tests/datetime/test_behavior.py
@@ -0,0 +1,172 @@
+from __future__ import annotations
+
+import pickle
+
+from copy import deepcopy
+from datetime import date
+from datetime import datetime
+from datetime import time
+
+import pytest
+
+import pendulum
+
+from pendulum import timezone
+from pendulum.tz.timezone import Timezone
+from pendulum.utils._compat import zoneinfo
+
+
+@pytest.fixture
+def p():
+ return pendulum.datetime(2016, 8, 27, 12, 34, 56, 123456, tz="Europe/Paris")
+
+
+@pytest.fixture
+def p1(p):
+ return p.in_tz("America/New_York")
+
+
+@pytest.fixture
+def dt():
+ tz = timezone("Europe/Paris")
+
+ return tz.convert(datetime(2016, 8, 27, 12, 34, 56, 123456))
+
+
+def test_timetuple(p, dt):
+ assert dt.timetuple() == p.timetuple()
+
+
+def test_utctimetuple(p, dt):
+ assert dt.utctimetuple() == p.utctimetuple()
+
+
+def test_date(p, dt):
+ assert p.date() == dt.date()
+
+
+def test_time(p, dt):
+ assert p.time() == dt.time()
+
+
+def test_timetz(p, dt):
+ assert p.timetz() == dt.timetz()
+
+
+def test_astimezone(p, dt, p1):
+ assert p.astimezone(p1.tzinfo) == dt.astimezone(p1.tzinfo)
+
+
+def test_ctime(p, dt):
+ assert p.ctime() == dt.ctime()
+
+
+def test_isoformat(p, dt):
+ assert p.isoformat() == dt.isoformat()
+
+
+def test_utcoffset(p, dt):
+ assert p.utcoffset() == dt.utcoffset()
+
+
+def test_tzname(p, dt):
+ assert p.tzname() == dt.tzname()
+
+
+def test_dst(p, dt):
+ assert p.dst() == dt.dst()
+
+
+def test_toordinal(p, dt):
+ assert p.toordinal() == dt.toordinal()
+
+
+def test_weekday(p, dt):
+ assert p.weekday() == dt.weekday()
+
+
+def test_isoweekday(p, dt):
+ assert p.isoweekday() == dt.isoweekday()
+
+
+def test_isocalendar(p, dt):
+ assert p.isocalendar() == dt.isocalendar()
+
+
+def test_fromtimestamp():
+ p = pendulum.DateTime.fromtimestamp(0, pendulum.UTC)
+ dt = datetime.fromtimestamp(0, pendulum.UTC)
+
+ assert p == dt
+
+
+def test_utcfromtimestamp():
+ p = pendulum.DateTime.utcfromtimestamp(0)
+ dt = datetime.utcfromtimestamp(0)
+
+ assert p == dt
+
+
+def test_fromordinal():
+ assert datetime.fromordinal(730120) == pendulum.DateTime.fromordinal(730120)
+
+
+def test_combine():
+ p = pendulum.DateTime.combine(date(2016, 1, 1), time(1, 2, 3, 123456))
+ dt = datetime.combine(date(2016, 1, 1), time(1, 2, 3, 123456))
+
+ assert p == dt
+
+
+def test_hash(p, dt):
+ assert hash(p) == hash(dt)
+
+ dt1 = pendulum.datetime(2016, 8, 27, 12, 34, 56, 123456, tz="Europe/Paris")
+ dt2 = pendulum.datetime(2016, 8, 27, 12, 34, 56, 123456, tz="Europe/Paris")
+ dt3 = pendulum.datetime(2016, 8, 27, 12, 34, 56, 123456, tz="America/Toronto")
+
+ assert hash(dt1) == hash(dt2)
+ assert hash(dt1) != hash(dt3)
+
+
+def test_pickle():
+ dt1 = pendulum.datetime(2016, 8, 27, 12, 34, 56, 123456, tz="Europe/Paris")
+ s = pickle.dumps(dt1)
+ dt2 = pickle.loads(s)
+
+ assert dt1 == dt2
+
+
+def test_pickle_with_integer_tzinfo():
+ dt1 = pendulum.datetime(2016, 8, 27, 12, 34, 56, 123456, tz=0)
+ s = pickle.dumps(dt1)
+ dt2 = pickle.loads(s)
+
+ assert dt1 == dt2
+
+
+def test_proper_dst():
+ dt = pendulum.datetime(1941, 7, 1, tz="Europe/Amsterdam")
+ native_dt = datetime(1941, 7, 1, tzinfo=zoneinfo.ZoneInfo("Europe/Amsterdam"))
+
+ assert dt.dst() == native_dt.dst()
+
+
+def test_deepcopy():
+ dt = pendulum.datetime(1941, 7, 1, tz="Europe/Amsterdam")
+
+ assert dt == deepcopy(dt)
+
+
+def test_pickle_timezone():
+ dt1 = pendulum.timezone("Europe/Amsterdam")
+ s = pickle.dumps(dt1)
+ dt2 = pickle.loads(s)
+
+ assert isinstance(dt2, Timezone)
+
+ dt1 = pendulum.timezone("UTC")
+ s = pickle.dumps(dt1)
+ dt2 = pickle.loads(s)
+
+ assert isinstance(dt2, Timezone)
diff --git a/tests/datetime/test_comparison.py b/tests/datetime/test_comparison.py
new file mode 100644
index 0000000..ad81e73
--- /dev/null
+++ b/tests/datetime/test_comparison.py
@@ -0,0 +1,394 @@
+from __future__ import annotations
+
+from datetime import datetime
+
+import pytz
+
+import pendulum
+
+from tests.conftest import assert_datetime
+
+
+def test_equal_to_true():
+ d1 = pendulum.datetime(2000, 1, 1, 1, 2, 3)
+ d2 = pendulum.datetime(2000, 1, 1, 1, 2, 3)
+ d3 = datetime(2000, 1, 1, 1, 2, 3, tzinfo=pendulum.UTC)
+
+ assert d2 == d1
+ assert d3 == d1
+
+
+def test_equal_to_false():
+ d1 = pendulum.datetime(2000, 1, 1, 1, 2, 3)
+ d2 = pendulum.datetime(2000, 1, 2, 1, 2, 3)
+ d3 = datetime(2000, 1, 2, 1, 2, 3, tzinfo=pendulum.UTC)
+
+ assert d2 != d1
+ assert d3 != d1
+
+
+def test_equal_with_timezone_true():
+ d1 = pendulum.datetime(2000, 1, 1, 12, 0, 0, tz="America/Toronto")
+ d2 = pendulum.datetime(2000, 1, 1, 9, 0, 0, tz="America/Vancouver")
+ d3 = datetime(2000, 1, 1, 12, 0, 0, tzinfo=pendulum.timezone("America/Toronto"))
+
+ assert d2 == d1
+ assert d3 == d1
+
+
+def test_equal_with_timezone_false():
+ d1 = pendulum.datetime(2000, 1, 1, tz="America/Toronto")
+ d2 = pendulum.datetime(2000, 1, 1, tz="America/Vancouver")
+ d3 = datetime(2000, 1, 1, tzinfo=pendulum.timezone("America/Toronto"))
+
+ assert d2 != d1
+ assert d3 == d1
+
+
+def test_not_equal_to_true():
+ d1 = pendulum.datetime(2000, 1, 1, 1, 2, 3)
+ d2 = pendulum.datetime(2000, 1, 2, 1, 2, 3)
+ d3 = datetime(2000, 1, 2, 1, 2, 3, tzinfo=pendulum.UTC)
+
+ assert d2 != d1
+ assert d3 != d1
+
+
+def test_not_equal_to_false():
+ d1 = pendulum.datetime(2000, 1, 1, 1, 2, 3)
+ d2 = pendulum.datetime(2000, 1, 1, 1, 2, 3)
+ d3 = datetime(2000, 1, 1, 1, 2, 3, tzinfo=pendulum.UTC)
+
+ assert d2 == d1
+ assert d3 == d1
+
+
+def test_not_equal_with_timezone_true():
+ d1 = pendulum.datetime(2000, 1, 1, tz="America/Toronto")
+ d2 = pendulum.datetime(2000, 1, 1, tz="America/Vancouver")
+ d3 = datetime(2000, 1, 1, tzinfo=pendulum.timezone("America/Toronto"))
+
+ assert d2 != d1
+ assert d3 == d1
+
+
+def test_not_equal_to_none():
+ d1 = pendulum.datetime(2000, 1, 1, 1, 2, 3)
+
+ assert d1 != None # noqa
+
+
+def test_greater_than_true():
+ d1 = pendulum.datetime(2000, 1, 1)
+ d2 = pendulum.datetime(1999, 12, 31)
+ d3 = datetime(1999, 12, 31, tzinfo=pendulum.UTC)
+
+ assert d1 > d2
+ assert d1 > d3
+
+
+def test_greater_than_false():
+ d1 = pendulum.datetime(2000, 1, 1)
+ d2 = pendulum.datetime(2000, 1, 2)
+ d3 = datetime(2000, 1, 2, tzinfo=pendulum.UTC)
+
+ assert not d1 > d2
+ assert not d1 > d3
+
+
+def test_greater_than_with_timezone_true():
+ d1 = pendulum.datetime(2000, 1, 1, 12, 0, 0, tz="America/Toronto")
+ d2 = pendulum.datetime(2000, 1, 1, 8, 59, 59, tz="America/Vancouver")
+ d3 = pytz.timezone("America/Vancouver").localize(datetime(2000, 1, 1, 8, 59, 59))
+
+ assert d1 > d2
+ assert d1 > d3
+
+
+def test_greater_than_with_timezone_false():
+ d1 = pendulum.datetime(2000, 1, 1, 12, 0, 0, tz="America/Toronto")
+ d2 = pendulum.datetime(2000, 1, 1, 9, 0, 1, tz="America/Vancouver")
+ d3 = pytz.timezone("America/Vancouver").localize(datetime(2000, 1, 1, 9, 0, 1))
+
+ assert not d1 > d2
+ assert not d1 > d3
+
+
+def test_greater_than_or_equal_true():
+ d1 = pendulum.datetime(2000, 1, 1)
+ d2 = pendulum.datetime(1999, 12, 31)
+ d3 = datetime(1999, 12, 31, tzinfo=pendulum.UTC)
+
+ assert d1 >= d2
+ assert d1 >= d3
+
+
+def test_greater_than_or_equal_true_equal():
+ d1 = pendulum.datetime(2000, 1, 1)
+ d2 = pendulum.datetime(2000, 1, 1)
+ d3 = datetime(2000, 1, 1, tzinfo=pendulum.UTC)
+
+ assert d1 >= d2
+ assert d1 >= d3
+
+
+def test_greater_than_or_equal_false():
+ d1 = pendulum.datetime(2000, 1, 1)
+ d2 = pendulum.datetime(2000, 1, 2)
+ d3 = datetime(2000, 1, 2, tzinfo=pendulum.UTC)
+
+ assert not d1 >= d2
+ assert not d1 >= d3
+
+
+def test_greater_than_or_equal_with_timezone_true():
+ d1 = pendulum.datetime(2000, 1, 1, 12, 0, 0, tz="America/Toronto")
+ d2 = pendulum.datetime(2000, 1, 1, 8, 59, 59, tz="America/Vancouver")
+ d3 = pytz.timezone("America/Vancouver").localize(datetime(2000, 1, 1, 8, 59, 59))
+
+ assert d1 >= d2
+ assert d1 >= d3
+
+
+def test_greater_than_or_equal_with_timezone_false():
+ d1 = pendulum.datetime(2000, 1, 1, 12, 0, 0, tz="America/Toronto")
+ d2 = pendulum.datetime(2000, 1, 1, 9, 0, 1, tz="America/Vancouver")
+ d3 = pytz.timezone("America/Vancouver").localize(datetime(2000, 1, 1, 9, 0, 1))
+
+ assert not d1 >= d2
+ assert not d1 >= d3
+
+
+def test_less_than_true():
+ d1 = pendulum.datetime(2000, 1, 1)
+ d2 = pendulum.datetime(2000, 1, 2)
+ d3 = datetime(2000, 1, 2, tzinfo=pendulum.UTC)
+
+ assert d1 < d2
+ assert d1 < d3
+
+
+def test_less_than_false():
+ d1 = pendulum.datetime(2000, 1, 2)
+ d2 = pendulum.datetime(2000, 1, 1)
+ d3 = datetime(2000, 1, 1, tzinfo=pendulum.UTC)
+
+ assert not d1 < d2
+ assert not d1 < d3
+
+
+def test_less_than_with_timezone_true():
+ d1 = pendulum.datetime(2000, 1, 1, 8, 59, 59, tz="America/Vancouver")
+ d2 = pendulum.datetime(2000, 1, 1, 12, 0, 0, tz="America/Toronto")
+ d3 = pytz.timezone("America/Toronto").localize(datetime(2000, 1, 1, 12, 0, 0))
+
+ assert d1 < d2
+ assert d1 < d3
+
+
+def test_less_than_with_timezone_false():
+ d1 = pendulum.datetime(2000, 1, 1, 9, 0, 1, tz="America/Vancouver")
+ d2 = pendulum.datetime(2000, 1, 1, 12, 0, 0, tz="America/Toronto")
+ d3 = pytz.timezone("America/Toronto").localize(datetime(2000, 1, 1, 12, 0, 0))
+
+ assert not d1 < d2
+ assert not d1 < d3
+
+
+def test_less_than_or_equal_true():
+ d1 = pendulum.datetime(2000, 1, 1)
+ d2 = pendulum.datetime(2000, 1, 2)
+ d3 = datetime(2000, 1, 2, tzinfo=pendulum.UTC)
+
+ assert d1 <= d2
+ assert d1 <= d3
+
+
+def test_less_than_or_equal_true_equal():
+ d1 = pendulum.datetime(2000, 1, 1)
+ d2 = pendulum.datetime(2000, 1, 1)
+ d3 = datetime(2000, 1, 1, tzinfo=pendulum.UTC)
+
+ assert d1 <= d2
+ assert d1 <= d3
+
+
+def test_less_than_or_equal_false():
+ d1 = pendulum.datetime(2000, 1, 2)
+ d2 = pendulum.datetime(2000, 1, 1)
+ d3 = datetime(2000, 1, 1, tzinfo=pendulum.UTC)
+
+ assert not d1 <= d2
+ assert not d1 <= d3
+
+
+def test_less_than_or_equal_with_timezone_true():
+ d1 = pendulum.datetime(2000, 1, 1, 8, 59, 59, tz="America/Vancouver")
+ d2 = pendulum.datetime(2000, 1, 1, 12, 0, 0, tz="America/Toronto")
+ d3 = pytz.timezone("America/Toronto").localize(datetime(2000, 1, 1, 12, 0, 0))
+
+ assert d1 <= d2
+ assert d1 <= d3
+
+
+def test_less_than_or_equal_with_timezone_false():
+ d1 = pendulum.datetime(2000, 1, 1, 9, 0, 1, tz="America/Vancouver")
+ d2 = pendulum.datetime(2000, 1, 1, 12, 0, 0, tz="America/Toronto")
+ d3 = pytz.timezone("America/Toronto").localize(datetime(2000, 1, 1, 12, 0, 0))
+
+ assert not d1 <= d2
+ assert not d1 <= d3
+
+
+def test_is_anniversary():
+ with pendulum.travel_to(pendulum.now()):
+ d = pendulum.now()
+ an_anniversary = d.subtract(years=1)
+ assert an_anniversary.is_anniversary()
+ not_an_anniversary = d.subtract(days=1)
+ assert not not_an_anniversary.is_anniversary()
+ also_not_an_anniversary = d.add(days=2)
+ assert not also_not_an_anniversary.is_anniversary()
+
+ d1 = pendulum.datetime(1987, 4, 23)
+ d2 = pendulum.datetime(2014, 9, 26)
+ d3 = pendulum.datetime(2014, 4, 23)
+ assert not d2.is_anniversary(d1)
+ assert d3.is_anniversary(d1)
+
+
+def test_is_birthday(): # backward compatibility
+ with pendulum.travel_to(pendulum.now()):
+ d = pendulum.now()
+ an_anniversary = d.subtract(years=1)
+ assert an_anniversary.is_birthday()
+ not_an_anniversary = d.subtract(days=1)
+ assert not not_an_anniversary.is_birthday()
+ also_not_an_anniversary = d.add(days=2)
+ assert not also_not_an_anniversary.is_birthday()
+
+ d1 = pendulum.datetime(1987, 4, 23)
+ d2 = pendulum.datetime(2014, 9, 26)
+ d3 = pendulum.datetime(2014, 4, 23)
+ assert not d2.is_birthday(d1)
+ assert d3.is_birthday(d1)
+
+
+def test_closest():
+ instance = pendulum.datetime(2015, 5, 28, 12, 0, 0)
+ dt1 = pendulum.datetime(2015, 5, 28, 11, 0, 0)
+ dt2 = pendulum.datetime(2015, 5, 28, 14, 0, 0)
+ closest = instance.closest(dt1, dt2)
+ assert closest == dt1
+
+ closest = instance.closest(dt2, dt1)
+ assert closest == dt1
+
+ dts = [
+ pendulum.datetime(2015, 5, 28, 16, 0, 0) + pendulum.duration(hours=x)
+ for x in range(4)
+ ]
+ closest = instance.closest(*dts)
+ assert closest == dts[0]
+
+ closest = instance.closest(*(dts[::-1]))
+ assert closest == dts[0]
+
+
+def test_closest_with_datetime():
+ instance = pendulum.datetime(2015, 5, 28, 12, 0, 0)
+ dt1 = datetime(2015, 5, 28, 11, 0, 0)
+ dt2 = datetime(2015, 5, 28, 14, 0, 0)
+ closest = instance.closest(dt1, dt2)
+ assert_datetime(closest, 2015, 5, 28, 11, 0, 0)
+
+ dts = [
+ pendulum.datetime(2015, 5, 28, 16, 0, 0) + pendulum.duration(hours=x)
+ for x in range(4)
+ ]
+ closest = instance.closest(dt1, dt2, *dts)
+
+ assert_datetime(closest, 2015, 5, 28, 11, 0, 0)
+
+
+def test_closest_with_equals():
+ instance = pendulum.datetime(2015, 5, 28, 12, 0, 0)
+ dt1 = pendulum.datetime(2015, 5, 28, 12, 0, 0)
+ dt2 = pendulum.datetime(2015, 5, 28, 14, 0, 0)
+ closest = instance.closest(dt1, dt2)
+ assert closest == dt1
+
+
+def test_farthest():
+ instance = pendulum.datetime(2015, 5, 28, 12, 0, 0)
+ dt1 = pendulum.datetime(2015, 5, 28, 11, 0, 0)
+ dt2 = pendulum.datetime(2015, 5, 28, 14, 0, 0)
+ farthest = instance.farthest(dt1, dt2)
+ assert farthest == dt2
+
+ farthest = instance.farthest(dt2, dt1)
+ assert farthest == dt2
+
+ dts = [
+ pendulum.datetime(2015, 5, 28, 16, 0, 0) + pendulum.duration(hours=x)
+ for x in range(4)
+ ]
+ farthest = instance.farthest(*dts)
+ assert farthest == dts[-1]
+
+ farthest = instance.farthest(*(dts[::-1]))
+ assert farthest == dts[-1]
+
+ f = pendulum.datetime(2010, 1, 1, 0, 0, 0)
+ assert f == instance.farthest(f, *(dts))
+
+
+def test_farthest_with_datetime():
+ instance = pendulum.datetime(2015, 5, 28, 12, 0, 0)
+ dt1 = datetime(2015, 5, 28, 11, 0, 0, tzinfo=pendulum.UTC)
+ dt2 = datetime(2015, 5, 28, 14, 0, 0, tzinfo=pendulum.UTC)
+ farthest = instance.farthest(dt1, dt2)
+ assert_datetime(farthest, 2015, 5, 28, 14, 0, 0)
+
+ dts = [
+ pendulum.datetime(2015, 5, 28, 16, 0, 0) + pendulum.duration(hours=x)
+ for x in range(4)
+ ]
+ farthest = instance.farthest(dt1, dt2, *dts)
+
+ assert_datetime(farthest, 2015, 5, 28, 19, 0, 0)
+
+
+def test_farthest_with_equals():
+ instance = pendulum.datetime(2015, 5, 28, 12, 0, 0)
+ dt1 = pendulum.datetime(2015, 5, 28, 12, 0, 0)
+ dt2 = pendulum.datetime(2015, 5, 28, 14, 0, 0)
+ farthest = instance.farthest(dt1, dt2)
+ assert farthest == dt2
+
+ dts = [
+ pendulum.datetime(2015, 5, 28, 16, 0, 0) + pendulum.duration(hours=x)
+ for x in range(4)
+ ]
+ farthest = instance.farthest(dt1, dt2, *dts)
+ assert farthest == dts[-1]
+
+
+def test_is_same_day():
+ dt1 = pendulum.datetime(2015, 5, 28, 12, 0, 0)
+ dt2 = pendulum.datetime(2015, 5, 29, 12, 0, 0)
+ dt3 = pendulum.datetime(2015, 5, 28, 12, 0, 0)
+ dt4 = datetime(2015, 5, 28, 12, 0, 0, tzinfo=pendulum.UTC)
+ dt5 = datetime(2015, 5, 29, 12, 0, 0, tzinfo=pendulum.UTC)
+
+ assert not dt1.is_same_day(dt2)
+ assert dt1.is_same_day(dt3)
+ assert dt1.is_same_day(dt4)
+ assert not dt1.is_same_day(dt5)
+
+
+def test_comparison_to_unsupported():
+ dt1 = pendulum.now()
+
+ assert dt1 != "test"
+ assert dt1 not in ["test"]
diff --git a/tests/datetime/test_construct.py b/tests/datetime/test_construct.py
new file mode 100644
index 0000000..9488c08
--- /dev/null
+++ b/tests/datetime/test_construct.py
@@ -0,0 +1,182 @@
+from __future__ import annotations
+
+import os
+
+from datetime import datetime
+
+import pytest
+import pytz
+
+from dateutil import tz
+
+import pendulum
+
+from pendulum import DateTime
+from pendulum.tz import timezone
+from pendulum.utils._compat import PYPY
+from tests.conftest import assert_datetime
+
+if not PYPY:
+ import time_machine
+else:
+ time_machine = None
+
+
+@pytest.fixture(autouse=True)
+def _setup():
+ yield
+
+ if os.getenv("TZ"):
+ del os.environ["TZ"]
+
+
+def test_creates_an_instance_default_to_utcnow():
+ now = pendulum.now("UTC")
+ p = pendulum.datetime(
+ now.year, now.month, now.day, now.hour, now.minute, now.second
+ )
+ assert now.timezone_name == p.timezone_name
+
+ assert_datetime(p, now.year, now.month, now.day, now.hour, now.minute, now.second)
+
+
+def test_setting_timezone():
+ tz = "Europe/London"
+ dtz = timezone(tz)
+ dt = datetime.utcnow()
+ offset = dtz.convert(dt).utcoffset().total_seconds() / 3600
+
+ p = pendulum.datetime(dt.year, dt.month, dt.day, tz=dtz)
+ assert p.timezone_name == tz
+ assert p.offset_hours == int(offset)
+
+
+def test_setting_timezone_with_string():
+ tz = "Europe/London"
+ dtz = timezone(tz)
+ dt = datetime.utcnow()
+ offset = dtz.convert(dt).utcoffset().total_seconds() / 3600
+
+ p = pendulum.datetime(dt.year, dt.month, dt.day, tz=tz)
+ assert p.timezone_name == tz
+ assert p.offset_hours == int(offset)
+
+
+def test_today():
+ today = pendulum.today()
+ assert isinstance(today, DateTime)
+
+
+def test_tomorrow():
+ now = pendulum.now().start_of("day")
+ tomorrow = pendulum.tomorrow()
+ assert isinstance(tomorrow, DateTime)
+ assert now.diff(tomorrow).in_days() == 1
+
+
+def test_yesterday():
+ now = pendulum.now().start_of("day")
+ yesterday = pendulum.yesterday()
+
+ assert isinstance(yesterday, DateTime)
+ assert now.diff(yesterday, False).in_days() == -1
+
+
+def test_instance_naive_datetime_defaults_to_utc():
+ now = pendulum.instance(datetime.now())
+ assert now.timezone_name == "UTC"
+
+
+def test_instance_timezone_aware_datetime():
+ now = pendulum.instance(datetime.now(timezone("Europe/Paris")))
+ assert now.timezone_name == "Europe/Paris"
+
+
+def test_instance_timezone_aware_datetime_pytz():
+ now = pendulum.instance(datetime.now(pytz.timezone("Europe/Paris")))
+ assert now.timezone_name == "Europe/Paris"
+
+
+def test_instance_timezone_aware_datetime_any_tzinfo():
+ dt = datetime(2016, 8, 7, 12, 34, 56, tzinfo=tz.gettz("Europe/Paris"))
+ now = pendulum.instance(dt)
+ assert now.timezone_name == "+02:00"
+
+
+def test_now():
+ now = pendulum.now("America/Toronto")
+ in_paris = pendulum.now("Europe/Paris")
+
+ assert now.hour != in_paris.hour
+
+
+if time_machine:
+
+ @time_machine.travel("2016-03-27 00:30:00Z", tick=False)
+ def test_now_dst_off():
+ utc = pendulum.now("UTC")
+ in_paris = pendulum.now("Europe/Paris")
+ in_paris_from_utc = utc.in_tz("Europe/Paris")
+ assert in_paris.hour == 1
+ assert not in_paris.is_dst()
+ assert in_paris.isoformat() == in_paris_from_utc.isoformat()
+
+ @time_machine.travel("2016-03-27 01:30:00Z", tick=False)
+ def test_now_dst_transitioning_on():
+ utc = pendulum.now("UTC")
+ in_paris = pendulum.now("Europe/Paris")
+ in_paris_from_utc = utc.in_tz("Europe/Paris")
+ assert in_paris.hour == 3
+ assert in_paris.is_dst()
+ assert in_paris.isoformat() == in_paris_from_utc.isoformat()
+
+ @time_machine.travel("2016-10-30 00:30:00Z", tick=False)
+ def test_now_dst_on():
+ utc = pendulum.now("UTC")
+ in_paris = pendulum.now("Europe/Paris")
+ in_paris_from_utc = utc.in_tz("Europe/Paris")
+ assert in_paris.hour == 2
+ assert in_paris.is_dst()
+ assert in_paris.isoformat() == in_paris_from_utc.isoformat()
+
+ @time_machine.travel("2016-10-30 01:30:00Z", tick=False)
+ def test_now_dst_transitioning_off():
+ utc = pendulum.now("UTC")
+ in_paris = pendulum.now("Europe/Paris")
+ in_paris_from_utc = utc.in_tz("Europe/Paris")
+ assert in_paris.hour == 2
+ assert not in_paris.is_dst()
+ assert in_paris.isoformat() == in_paris_from_utc.isoformat()
+
+
+def test_now_with_fixed_offset():
+ now = pendulum.now(6)
+
+ assert now.timezone_name == "+06:00"
+
+
+def test_create_with_no_transition_timezone():
+ dt = pendulum.now("Etc/UTC")
+
+ assert dt.timezone_name == "Etc/UTC"
+
+
+def test_create_maintains_microseconds():
+ d = pendulum.datetime(2016, 11, 12, 2, 9, 39, 594000, tz="America/Panama")
+ assert_datetime(d, 2016, 11, 12, 2, 9, 39, 594000)
+
+ d = pendulum.datetime(2316, 11, 12, 2, 9, 39, 857, tz="America/Panama")
+ assert_datetime(d, 2316, 11, 12, 2, 9, 39, 857)
+
+
+def test_second_inaccuracy_on_past_datetimes():
+ dt = pendulum.datetime(1901, 12, 13, 0, 0, 0, 555555, tz="US/Central")
+
+ assert_datetime(dt, 1901, 12, 13, 0, 0, 0, 555555)
+
+
+def test_local():
+ local = pendulum.local(2018, 2, 2, 12, 34, 56, 123456)
+
+ assert_datetime(local, 2018, 2, 2, 12, 34, 56, 123456)
+ assert local.timezone_name == "America/Toronto"
diff --git a/tests/datetime/test_create_from_timestamp.py b/tests/datetime/test_create_from_timestamp.py
new file mode 100644
index 0000000..121a7c2
--- /dev/null
+++ b/tests/datetime/test_create_from_timestamp.py
@@ -0,0 +1,24 @@
+from __future__ import annotations
+
+import pendulum
+
+from pendulum import timezone
+from tests.conftest import assert_datetime
+
+
+def test_create_from_timestamp_returns_pendulum():
+ d = pendulum.from_timestamp(pendulum.datetime(1975, 5, 21, 22, 32, 5).timestamp())
+ assert_datetime(d, 1975, 5, 21, 22, 32, 5)
+ assert d.timezone_name == "UTC"
+
+
+def test_create_from_timestamp_with_timezone_string():
+ d = pendulum.from_timestamp(0, "America/Toronto")
+ assert d.timezone_name == "America/Toronto"
+ assert_datetime(d, 1969, 12, 31, 19, 0, 0)
+
+
+def test_create_from_timestamp_with_timezone():
+ d = pendulum.from_timestamp(0, timezone("America/Toronto"))
+ assert d.timezone_name == "America/Toronto"
+ assert_datetime(d, 1969, 12, 31, 19, 0, 0)
diff --git a/tests/datetime/test_day_of_week_modifiers.py b/tests/datetime/test_day_of_week_modifiers.py
new file mode 100644
index 0000000..46de84e
--- /dev/null
+++ b/tests/datetime/test_day_of_week_modifiers.py
@@ -0,0 +1,314 @@
+from __future__ import annotations
+
+import pytest
+
+import pendulum
+
+from pendulum.exceptions import PendulumException
+from tests.conftest import assert_datetime
+
+
+def test_start_of_week():
+ d = pendulum.datetime(1980, 8, 7, 12, 11, 9).start_of("week")
+ assert_datetime(d, 1980, 8, 4, 0, 0, 0)
+
+
+def test_start_of_week_from_week_start():
+ d = pendulum.datetime(1980, 8, 4).start_of("week")
+ assert_datetime(d, 1980, 8, 4, 0, 0, 0)
+
+
+def test_start_of_week_crossing_year_boundary():
+ d = pendulum.datetime(2014, 1, 1).start_of("week")
+ assert_datetime(d, 2013, 12, 30, 0, 0, 0)
+
+
+def test_end_of_week():
+ d = pendulum.datetime(1980, 8, 7, 12, 11, 9).end_of("week")
+ assert_datetime(d, 1980, 8, 10, 23, 59, 59)
+
+
+def test_end_of_week_from_week_end():
+ d = pendulum.datetime(1980, 8, 10).end_of("week")
+ assert_datetime(d, 1980, 8, 10, 23, 59, 59)
+
+
+def test_end_of_week_crossing_year_boundary():
+ d = pendulum.datetime(2013, 12, 31).end_of("week")
+ assert_datetime(d, 2014, 1, 5, 23, 59, 59)
+
+
+def test_next():
+ d = pendulum.datetime(1975, 5, 21).next()
+ assert_datetime(d, 1975, 5, 28, 0, 0, 0)
+
+
+def test_next_monday():
+ d = pendulum.datetime(1975, 5, 21).next(pendulum.MONDAY)
+ assert_datetime(d, 1975, 5, 26, 0, 0, 0)
+
+
+def test_next_saturday():
+ d = pendulum.datetime(1975, 5, 21).next(6)
+ assert_datetime(d, 1975, 5, 24, 0, 0, 0)
+
+
+def test_next_keep_time():
+ d = pendulum.datetime(1975, 5, 21, 12).next()
+ assert_datetime(d, 1975, 5, 28, 0, 0, 0)
+
+ d = pendulum.datetime(1975, 5, 21, 12).next(keep_time=True)
+ assert_datetime(d, 1975, 5, 28, 12, 0, 0)
+
+
+def test_next_invalid():
+ dt = pendulum.datetime(1975, 5, 21, 12)
+
+ with pytest.raises(ValueError):
+ dt.next(7)
+
+
+def test_previous():
+ d = pendulum.datetime(1975, 5, 21).previous()
+ assert_datetime(d, 1975, 5, 14, 0, 0, 0)
+
+
+def test_previous_monday():
+ d = pendulum.datetime(1975, 5, 21).previous(pendulum.MONDAY)
+ assert_datetime(d, 1975, 5, 19, 0, 0, 0)
+
+
+def test_previous_saturday():
+ d = pendulum.datetime(1975, 5, 21).previous(6)
+ assert_datetime(d, 1975, 5, 17, 0, 0, 0)
+
+
+def test_previous_keep_time():
+ d = pendulum.datetime(1975, 5, 21, 12).previous()
+ assert_datetime(d, 1975, 5, 14, 0, 0, 0)
+
+ d = pendulum.datetime(1975, 5, 21, 12).previous(keep_time=True)
+ assert_datetime(d, 1975, 5, 14, 12, 0, 0)
+
+
+def test_previous_invalid():
+ dt = pendulum.datetime(1975, 5, 21, 12)
+
+ with pytest.raises(ValueError):
+ dt.previous(7)
+
+
+def test_first_day_of_month():
+ d = pendulum.datetime(1975, 11, 21).first_of("month")
+ assert_datetime(d, 1975, 11, 1, 0, 0, 0)
+
+
+def test_first_wednesday_of_month():
+ d = pendulum.datetime(1975, 11, 21).first_of("month", pendulum.WEDNESDAY)
+ assert_datetime(d, 1975, 11, 5, 0, 0, 0)
+
+
+def test_first_friday_of_month():
+ d = pendulum.datetime(1975, 11, 21).first_of("month", 5)
+ assert_datetime(d, 1975, 11, 7, 0, 0, 0)
+
+
+def test_last_day_of_month():
+ d = pendulum.datetime(1975, 12, 5).last_of("month")
+ assert_datetime(d, 1975, 12, 31, 0, 0, 0)
+
+
+def test_last_tuesday_of_month():
+ d = pendulum.datetime(1975, 12, 1).last_of("month", pendulum.TUESDAY)
+ assert_datetime(d, 1975, 12, 30, 0, 0, 0)
+
+
+def test_last_friday_of_month():
+ d = pendulum.datetime(1975, 12, 5).last_of("month", 5)
+ assert_datetime(d, 1975, 12, 26, 0, 0, 0)
+
+
+def test_nth_of_month_outside_scope():
+ d = pendulum.datetime(1975, 6, 5)
+
+ with pytest.raises(PendulumException):
+ d.nth_of("month", 6, pendulum.MONDAY)
+
+
+def test_nth_of_month_outside_year():
+ d = pendulum.datetime(1975, 12, 5)
+
+ with pytest.raises(PendulumException):
+ d.nth_of("month", 55, pendulum.MONDAY)
+
+
+def test_nth_of_month_first():
+ d = pendulum.datetime(1975, 12, 5).nth_of("month", 1, pendulum.MONDAY)
+
+ assert_datetime(d, 1975, 12, 1, 0, 0, 0)
+
+
+def test_2nd_monday_of_month():
+ d = pendulum.datetime(1975, 12, 5).nth_of("month", 2, pendulum.MONDAY)
+
+ assert_datetime(d, 1975, 12, 8, 0, 0, 0)
+
+
+def test_3rd_wednesday_of_month():
+ d = pendulum.datetime(1975, 12, 5).nth_of("month", 3, 3)
+
+ assert_datetime(d, 1975, 12, 17, 0, 0, 0)
+
+
+def test_first_day_of_quarter():
+ d = pendulum.datetime(1975, 11, 21).first_of("quarter")
+ assert_datetime(d, 1975, 10, 1, 0, 0, 0)
+
+
+def test_first_wednesday_of_quarter():
+ d = pendulum.datetime(1975, 11, 21).first_of("quarter", pendulum.WEDNESDAY)
+ assert_datetime(d, 1975, 10, 1, 0, 0, 0)
+
+
+def test_first_friday_of_quarter():
+ d = pendulum.datetime(1975, 11, 21).first_of("quarter", 5)
+ assert_datetime(d, 1975, 10, 3, 0, 0, 0)
+
+
+def test_first_of_quarter_from_a_day_that_will_not_exist_in_the_first_month():
+ d = pendulum.datetime(2014, 5, 31).first_of("quarter")
+ assert_datetime(d, 2014, 4, 1, 0, 0, 0)
+
+
+def test_last_day_of_quarter():
+ d = pendulum.datetime(1975, 8, 5).last_of("quarter")
+ assert_datetime(d, 1975, 9, 30, 0, 0, 0)
+
+
+def test_last_tuesday_of_quarter():
+ d = pendulum.datetime(1975, 8, 5).last_of("quarter", pendulum.TUESDAY)
+ assert_datetime(d, 1975, 9, 30, 0, 0, 0)
+
+
+def test_last_friday_of_quarter():
+ d = pendulum.datetime(1975, 8, 5).last_of("quarter", pendulum.FRIDAY)
+ assert_datetime(d, 1975, 9, 26, 0, 0, 0)
+
+
+def test_last_day_of_quarter_that_will_not_exist_in_the_last_month():
+ d = pendulum.datetime(2014, 5, 31).last_of("quarter")
+ assert_datetime(d, 2014, 6, 30, 0, 0, 0)
+
+
+def test_nth_of_quarter_outside_scope():
+ d = pendulum.datetime(1975, 1, 5)
+
+ with pytest.raises(PendulumException):
+ d.nth_of("quarter", 20, pendulum.MONDAY)
+
+
+def test_nth_of_quarter_outside_year():
+ d = pendulum.datetime(1975, 1, 5)
+
+ with pytest.raises(PendulumException):
+ d.nth_of("quarter", 55, pendulum.MONDAY)
+
+
+def test_nth_of_quarter_first():
+ d = pendulum.datetime(1975, 12, 5).nth_of("quarter", 1, pendulum.MONDAY)
+
+ assert_datetime(d, 1975, 10, 6, 0, 0, 0)
+
+
+def test_nth_of_quarter_from_a_day_that_will_not_exist_in_the_first_month():
+ d = pendulum.datetime(2014, 5, 31).nth_of("quarter", 2, pendulum.MONDAY)
+ assert_datetime(d, 2014, 4, 14, 0, 0, 0)
+
+
+def test_2nd_monday_of_quarter():
+ d = pendulum.datetime(1975, 8, 5).nth_of("quarter", 2, pendulum.MONDAY)
+ assert_datetime(d, 1975, 7, 14, 0, 0, 0)
+
+
+def test_3rd_wednesday_of_quarter():
+ d = pendulum.datetime(1975, 8, 5).nth_of("quarter", 3, 3)
+ assert_datetime(d, 1975, 7, 16, 0, 0, 0)
+
+
+def test_first_day_of_year():
+ d = pendulum.datetime(1975, 11, 21).first_of("year")
+ assert_datetime(d, 1975, 1, 1, 0, 0, 0)
+
+
+def test_first_wednesday_of_year():
+ d = pendulum.datetime(1975, 11, 21).first_of("year", pendulum.WEDNESDAY)
+ assert_datetime(d, 1975, 1, 1, 0, 0, 0)
+
+
+def test_first_friday_of_year():
+ d = pendulum.datetime(1975, 11, 21).first_of("year", 5)
+ assert_datetime(d, 1975, 1, 3, 0, 0, 0)
+
+
+def test_last_day_of_year():
+ d = pendulum.datetime(1975, 8, 5).last_of("year")
+ assert_datetime(d, 1975, 12, 31, 0, 0, 0)
+
+
+def test_last_tuesday_of_year():
+ d = pendulum.datetime(1975, 8, 5).last_of("year", pendulum.TUESDAY)
+ assert_datetime(d, 1975, 12, 30, 0, 0, 0)
+
+
+def test_last_friday_of_year():
+ d = pendulum.datetime(1975, 8, 5).last_of("year", 5)
+ assert_datetime(d, 1975, 12, 26, 0, 0, 0)
+
+
+def test_nth_of_year_outside_scope():
+ d = pendulum.datetime(1975, 1, 5)
+
+ with pytest.raises(PendulumException):
+ d.nth_of("year", 55, pendulum.MONDAY)
+
+
+def test_nth_of_year_first():
+ d = pendulum.datetime(1975, 12, 5).nth_of("year", 1, pendulum.MONDAY)
+
+ assert_datetime(d, 1975, 1, 6, 0, 0, 0)
+
+
+def test_2nd_monday_of_year():
+ d = pendulum.datetime(1975, 8, 5).nth_of("year", 2, pendulum.MONDAY)
+ assert_datetime(d, 1975, 1, 13, 0, 0, 0)
+
+
+def test_2rd_wednesday_of_year():
+ d = pendulum.datetime(1975, 8, 5).nth_of("year", 3, pendulum.WEDNESDAY)
+ assert_datetime(d, 1975, 1, 15, 0, 0, 0)
+
+
+def test_7th_thursday_of_year():
+ d = pendulum.datetime(1975, 8, 31).nth_of("year", 7, pendulum.THURSDAY)
+ assert_datetime(d, 1975, 2, 13, 0, 0, 0)
+
+
+def test_first_of_invalid_unit():
+ d = pendulum.datetime(1975, 8, 5)
+
+ with pytest.raises(ValueError):
+ d.first_of("invalid")
+
+
+def test_last_of_invalid_unit():
+ d = pendulum.datetime(1975, 8, 5)
+
+ with pytest.raises(ValueError):
+ d.last_of("invalid")
+
+
+def test_nth_of_invalid_unit():
+ d = pendulum.datetime(1975, 8, 5)
+
+ with pytest.raises(ValueError):
+ d.nth_of("invalid", 3, pendulum.MONDAY)
diff --git a/tests/datetime/test_diff.py b/tests/datetime/test_diff.py
new file mode 100644
index 0000000..7a31507
--- /dev/null
+++ b/tests/datetime/test_diff.py
@@ -0,0 +1,880 @@
+from __future__ import annotations
+
+from datetime import datetime
+
+import pytest
+
+import pendulum
+
+
+def test_diff_in_years_positive():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.add(years=1)).in_years() == 1
+
+
+def test_diff_in_years_negative_with_sign():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.subtract(years=1), False).in_years() == -1
+
+
+def test_diff_in_years_negative_no_sign():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.subtract(years=1)).in_years() == 1
+
+
+def test_diff_in_years_vs_default_now():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(years=1).diff().in_years() == 1
+
+
+def test_diff_in_years_ensure_is_truncated():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.add(years=1).add(months=7)).in_years() == 1
+
+
+def test_diff_in_months_positive():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.add(years=1).add(months=1)).in_months() == 13
+
+
+def test_diff_in_months_negative_with_sign():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.subtract(years=1).add(months=1), False).in_months() == -11
+
+
+def test_diff_in_months_negative_no_sign():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.subtract(years=1).add(months=1)).in_months() == 11
+
+
+def test_diff_in_months_vs_default_now():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(years=1).diff().in_months() == 12
+
+
+def test_diff_in_months_ensure_is_truncated():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.add(months=1).add(days=16)).in_months() == 1
+
+
+def test_diff_in_days_positive():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.add(years=1)).in_days() == 366
+
+
+def test_diff_in_days_negative_with_sign():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.subtract(years=1), False).in_days() == -365
+
+
+def test_diff_in_days_negative_no_sign():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.subtract(years=1)).in_days() == 365
+
+
+def test_diff_in_days_vs_default_now():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(weeks=1).diff().in_days() == 7
+
+
+def test_diff_in_days_ensure_is_truncated():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.add(days=1).add(hours=13)).in_days() == 1
+
+
+def test_diff_in_weeks_positive():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.add(years=1)).in_weeks() == 52
+
+
+def test_diff_in_weeks_negative_with_sign():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.subtract(years=1), False).in_weeks() == -52
+
+
+def test_diff_in_weeks_negative_no_sign():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.subtract(years=1)).in_weeks() == 52
+
+
+def test_diff_in_weeks_vs_default_now():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(weeks=1).diff().in_weeks() == 1
+
+
+def test_diff_in_weeks_ensure_is_truncated():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.add(weeks=1).subtract(days=1)).in_weeks() == 0
+
+
+def test_diff_in_hours_positive():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.add(days=1).add(hours=2)).in_hours() == 26
+
+
+def test_diff_in_hours_negative_with_sign():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.subtract(days=1).add(hours=2), False).in_hours() == -22
+
+
+def test_diff_in_hours_negative_no_sign():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.subtract(days=1).add(hours=2)).in_hours() == 22
+
+
+def test_diff_in_hours_vs_default_now():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 15), freeze=True):
+ assert pendulum.now().subtract(days=2).diff().in_hours() == 48
+
+
+def test_diff_in_hours_ensure_is_truncated():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.add(hours=1).add(minutes=31)).in_hours() == 1
+
+
+def test_diff_in_minutes_positive():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.add(hours=1).add(minutes=2)).in_minutes() == 62
+
+
+def test_diff_in_minutes_positive_big():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.add(hours=25).add(minutes=2)).in_minutes() == 1502
+
+
+def test_diff_in_minutes_negative_with_sign():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.subtract(hours=1).add(minutes=2), False).in_minutes() == -58
+
+
+def test_diff_in_minutes_negative_no_sign():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.subtract(hours=1).add(minutes=2)).in_minutes() == 58
+
+
+def test_diff_in_minutes_vs_default_now():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(hours=1).diff().in_minutes() == 60
+
+
+def test_diff_in_minutes_ensure_is_truncated():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.add(minutes=1).add(seconds=59)).in_minutes() == 1
+
+
+def test_diff_in_seconds_positive():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.add(minutes=1).add(seconds=2)).in_seconds() == 62
+
+
+def test_diff_in_seconds_positive_big():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.add(hours=2).add(seconds=2)).in_seconds() == 7202
+
+
+def test_diff_in_seconds_negative_with_sign():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.subtract(minutes=1).add(seconds=2), False).in_seconds() == -58
+
+
+def test_diff_in_seconds_negative_no_sign():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.subtract(minutes=1).add(seconds=2)).in_seconds() == 58
+
+
+def test_diff_in_seconds_vs_default_now():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(hours=1).diff().in_seconds() == 3600
+
+
+def test_diff_in_seconds_ensure_is_truncated():
+ dt = pendulum.datetime(2000, 1, 1)
+ assert dt.diff(dt.add(seconds=1.9)).in_seconds() == 1
+
+
+def test_diff_in_seconds_with_timezones():
+ dt_ottawa = pendulum.datetime(2000, 1, 1, 13, tz="America/Toronto")
+ dt_vancouver = pendulum.datetime(2000, 1, 1, 13, tz="America/Vancouver")
+ assert dt_ottawa.diff(dt_vancouver).in_seconds() == 3 * 60 * 60
+
+
+def test_diff_for_humans_now_and_second():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().diff_for_humans() == "a few seconds ago"
+
+
+def test_diff_for_humans_now_and_second_with_timezone():
+ van_now = pendulum.now("America/Vancouver")
+ here_now = van_now.in_timezone(pendulum.now().timezone)
+
+ with pendulum.travel_to(here_now, freeze=True):
+ assert here_now.diff_for_humans() == "a few seconds ago"
+
+
+def test_diff_for_humans_now_and_seconds():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().subtract(seconds=2).diff_for_humans() == "a few seconds ago"
+ )
+
+
+def test_diff_for_humans_now_and_nearly_minute():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(seconds=59).diff_for_humans() == "59 seconds ago"
+
+
+def test_diff_for_humans_now_and_minute():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(minutes=1).diff_for_humans() == "1 minute ago"
+
+
+def test_diff_for_humans_now_and_minutes():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(minutes=2).diff_for_humans() == "2 minutes ago"
+
+
+def test_diff_for_humans_now_and_nearly_hour():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(minutes=59).diff_for_humans() == "59 minutes ago"
+
+
+def test_diff_for_humans_now_and_hour():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(hours=1).diff_for_humans() == "1 hour ago"
+
+
+def test_diff_for_humans_now_and_hours():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(hours=2).diff_for_humans() == "2 hours ago"
+
+
+def test_diff_for_humans_now_and_nearly_day():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(hours=23).diff_for_humans() == "23 hours ago"
+
+
+def test_diff_for_humans_now_and_day():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(days=1).diff_for_humans() == "1 day ago"
+
+
+def test_diff_for_humans_now_and_days():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(days=2).diff_for_humans() == "2 days ago"
+
+
+def test_diff_for_humans_now_and_nearly_week():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(days=6).diff_for_humans() == "6 days ago"
+
+
+def test_diff_for_humans_now_and_week():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(weeks=1).diff_for_humans() == "1 week ago"
+
+
+def test_diff_for_humans_now_and_weeks():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(weeks=2).diff_for_humans() == "2 weeks ago"
+
+
+def test_diff_for_humans_now_and_nearly_month():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(weeks=3).diff_for_humans() == "3 weeks ago"
+
+
+def test_diff_for_humans_now_and_month():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(weeks=4).diff_for_humans() == "4 weeks ago"
+ assert pendulum.now().subtract(months=1).diff_for_humans() == "1 month ago"
+
+
+def test_diff_for_humans_now_and_months():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(months=2).diff_for_humans() == "2 months ago"
+
+
+def test_diff_for_humans_now_and_nearly_year():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(months=11).diff_for_humans() == "11 months ago"
+
+
+def test_diff_for_humans_now_and_year():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(years=1).diff_for_humans() == "1 year ago"
+
+
+def test_diff_for_humans_now_and_years():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().subtract(years=2).diff_for_humans() == "2 years ago"
+
+
+def test_diff_for_humans_now_and_future_second():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(seconds=1).diff_for_humans() == "in a few seconds"
+
+
+def test_diff_for_humans_now_and_future_seconds():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(seconds=2).diff_for_humans() == "in a few seconds"
+
+
+def test_diff_for_humans_now_and_nearly_future_minute():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(seconds=59).diff_for_humans() == "in 59 seconds"
+
+
+def test_diff_for_humans_now_and_future_minute():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(minutes=1).diff_for_humans() == "in 1 minute"
+
+
+def test_diff_for_humans_now_and_future_minutes():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(minutes=2).diff_for_humans() == "in 2 minutes"
+
+
+def test_diff_for_humans_now_and_nearly_future_hour():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(minutes=59).diff_for_humans() == "in 59 minutes"
+
+
+def test_diff_for_humans_now_and_future_hour():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(hours=1).diff_for_humans() == "in 1 hour"
+
+
+def test_diff_for_humans_now_and_future_hours():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(hours=2).diff_for_humans() == "in 2 hours"
+
+
+def test_diff_for_humans_now_and_nearly_future_day():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(hours=23).diff_for_humans() == "in 23 hours"
+
+
+def test_diff_for_humans_now_and_future_day():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(days=1).diff_for_humans() == "in 1 day"
+
+
+def test_diff_for_humans_now_and_future_days():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(days=2).diff_for_humans() == "in 2 days"
+
+
+def test_diff_for_humans_now_and_nearly_future_week():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(days=6).diff_for_humans() == "in 6 days"
+
+
+def test_diff_for_humans_now_and_future_week():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(weeks=1).diff_for_humans() == "in 1 week"
+
+
+def test_diff_for_humans_now_and_future_weeks():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(weeks=2).diff_for_humans() == "in 2 weeks"
+
+
+def test_diff_for_humans_now_and_nearly_future_month():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(weeks=3).diff_for_humans() == "in 3 weeks"
+
+
+def test_diff_for_humans_now_and_future_month():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(weeks=4).diff_for_humans() == "in 4 weeks"
+ assert pendulum.now().add(months=1).diff_for_humans() == "in 1 month"
+
+
+def test_diff_for_humans_now_and_future_months():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(months=2).diff_for_humans() == "in 2 months"
+
+
+def test_diff_for_humans_now_and_nearly_future_year():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(months=11).diff_for_humans() == "in 11 months"
+
+
+def test_diff_for_humans_now_and_future_year():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(years=1).diff_for_humans() == "in 1 year"
+
+
+def test_diff_for_humans_now_and_future_years():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert pendulum.now().add(years=2).diff_for_humans() == "in 2 years"
+
+
+def test_diff_for_humans_other_and_second():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(seconds=1))
+ == "a few seconds before"
+ )
+
+
+def test_diff_for_humans_other_and_seconds():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(seconds=2))
+ == "a few seconds before"
+ )
+
+
+def test_diff_for_humans_other_and_nearly_minute():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(seconds=59))
+ == "59 seconds before"
+ )
+
+
+def test_diff_for_humans_other_and_minute():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(minutes=1))
+ == "1 minute before"
+ )
+
+
+def test_diff_for_humans_other_and_minutes():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(minutes=2))
+ == "2 minutes before"
+ )
+
+
+def test_diff_for_humans_other_and_nearly_hour():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(minutes=59))
+ == "59 minutes before"
+ )
+
+
+def test_diff_for_humans_other_and_hour():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(hours=1))
+ == "1 hour before"
+ )
+
+
+def test_diff_for_humans_other_and_hours():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(hours=2))
+ == "2 hours before"
+ )
+
+
+def test_diff_for_humans_other_and_nearly_day():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(hours=23))
+ == "23 hours before"
+ )
+
+
+def test_diff_for_humans_other_and_day():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(days=1)) == "1 day before"
+ )
+
+
+def test_diff_for_humans_other_and_days():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(days=2))
+ == "2 days before"
+ )
+
+
+def test_diff_for_humans_other_and_nearly_week():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(days=6))
+ == "6 days before"
+ )
+
+
+def test_diff_for_humans_other_and_week():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(weeks=1))
+ == "1 week before"
+ )
+
+
+def test_diff_for_humans_other_and_weeks():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(weeks=2))
+ == "2 weeks before"
+ )
+
+
+def test_diff_for_humans_other_and_nearly_month():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(weeks=3))
+ == "3 weeks before"
+ )
+
+
+def test_diff_for_humans_other_and_month():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(weeks=4))
+ == "4 weeks before"
+ )
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(months=1))
+ == "1 month before"
+ )
+
+
+def test_diff_for_humans_other_and_months():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(months=2))
+ == "2 months before"
+ )
+
+
+def test_diff_for_humans_other_and_nearly_year():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(months=11))
+ == "11 months before"
+ )
+
+
+def test_diff_for_humans_other_and_year():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(years=1))
+ == "1 year before"
+ )
+
+
+def test_diff_for_humans_other_and_years():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(years=2))
+ == "2 years before"
+ )
+
+
+def test_diff_for_humans_other_and_future_second():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(seconds=1))
+ == "a few seconds after"
+ )
+
+
+def test_diff_for_humans_other_and_future_seconds():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(seconds=2))
+ == "a few seconds after"
+ )
+
+
+def test_diff_for_humans_other_and_nearly_future_minute():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(seconds=59))
+ == "59 seconds after"
+ )
+
+
+def test_diff_for_humans_other_and_future_minute():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(minutes=1))
+ == "1 minute after"
+ )
+
+
+def test_diff_for_humans_other_and_future_minutes():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(minutes=2))
+ == "2 minutes after"
+ )
+
+
+def test_diff_for_humans_other_and_nearly_future_hour():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(minutes=59))
+ == "59 minutes after"
+ )
+
+
+def test_diff_for_humans_other_and_future_hour():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(hours=1))
+ == "1 hour after"
+ )
+
+
+def test_diff_for_humans_other_and_future_hours():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(hours=2))
+ == "2 hours after"
+ )
+
+
+def test_diff_for_humans_other_and_nearly_future_day():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(hours=23))
+ == "23 hours after"
+ )
+
+
+def test_diff_for_humans_other_and_future_day():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(days=1))
+ == "1 day after"
+ )
+
+
+def test_diff_for_humans_other_and_future_days():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(days=2))
+ == "2 days after"
+ )
+
+
+def test_diff_for_humans_other_and_nearly_future_week():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(days=6))
+ == "6 days after"
+ )
+
+
+def test_diff_for_humans_other_and_future_week():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(weeks=1))
+ == "1 week after"
+ )
+
+
+def test_diff_for_humans_other_and_future_weeks():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(weeks=2))
+ == "2 weeks after"
+ )
+
+
+def test_diff_for_humans_other_and_nearly_future_month():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(weeks=3))
+ == "3 weeks after"
+ )
+
+
+def test_diff_for_humans_other_and_future_month():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(weeks=4))
+ == "4 weeks after"
+ )
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(months=1))
+ == "1 month after"
+ )
+
+
+def test_diff_for_humans_other_and_future_months():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(months=2))
+ == "2 months after"
+ )
+
+
+def test_diff_for_humans_other_and_nearly_future_year():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(months=11))
+ == "11 months after"
+ )
+
+
+def test_diff_for_humans_other_and_future_year():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(years=1))
+ == "1 year after"
+ )
+
+
+def test_diff_for_humans_other_and_future_years():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(years=2))
+ == "2 years after"
+ )
+
+
+def test_diff_for_humans_absolute_seconds():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(seconds=59), True)
+ == "59 seconds"
+ )
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(seconds=59), True)
+ == "59 seconds"
+ )
+
+
+def test_diff_for_humans_absolute_minutes():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(minutes=30), True)
+ == "30 minutes"
+ )
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(minutes=30), True)
+ == "30 minutes"
+ )
+
+
+def test_diff_for_humans_absolute_hours():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(hours=3), True)
+ == "3 hours"
+ )
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(hours=3), True)
+ == "3 hours"
+ )
+
+
+def test_diff_for_humans_absolute_days():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(days=2), True)
+ == "2 days"
+ )
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(days=2), True) == "2 days"
+ )
+
+
+def test_diff_for_humans_absolute_weeks():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(weeks=2), True)
+ == "2 weeks"
+ )
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(weeks=2), True)
+ == "2 weeks"
+ )
+
+
+def test_diff_for_humans_absolute_months():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(months=2), True)
+ == "2 months"
+ )
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(months=2), True)
+ == "2 months"
+ )
+
+
+def test_diff_for_humans_absolute_years():
+ with pendulum.travel_to(pendulum.datetime(2012, 1, 1, 1, 2, 3), freeze=True):
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().subtract(years=1), True)
+ == "1 year"
+ )
+ assert (
+ pendulum.now().diff_for_humans(pendulum.now().add(years=1), True)
+ == "1 year"
+ )
+
+
+def test_diff_for_humans_accuracy():
+ now = pendulum.now("utc")
+
+ with pendulum.travel_to(now.add(microseconds=200), freeze=True):
+ assert now.add(years=1).diff_for_humans(absolute=True) == "1 year"
+ assert now.add(months=11).diff_for_humans(absolute=True) == "11 months"
+ assert now.add(days=27).diff_for_humans(absolute=True) == "4 weeks"
+ assert now.add(years=1, months=3).diff_for_humans(absolute=True) == "1 year"
+ assert now.add(years=1, months=8).diff_for_humans(absolute=True) == "2 years"
+
+ # DST
+ now = pendulum.datetime(2017, 3, 7, tz="America/Toronto")
+ with pendulum.travel_to(now, freeze=True):
+ assert now.add(days=6).diff_for_humans(absolute=True) == "6 days"
+
+
+def test_subtraction():
+ d = pendulum.naive(2016, 7, 5, 12, 32, 25, 0)
+ future_dt = datetime(2016, 7, 5, 13, 32, 25, 0)
+ future = d.add(hours=1)
+
+ assert (future - d).total_seconds() == 3600
+ assert (future_dt - d).total_seconds() == 3600
+
+
+def test_subtraction_aware_naive():
+ dt = pendulum.datetime(2016, 7, 5, 12, 32, 25, 0)
+ future_dt = datetime(2016, 7, 5, 13, 32, 25, 0)
+
+ with pytest.raises(TypeError):
+ future_dt - dt
+
+ future_dt = pendulum.naive(2016, 7, 5, 13, 32, 25, 0)
+
+ with pytest.raises(TypeError):
+ future_dt - dt
+
+
+def test_subtraction_with_timezone():
+ dt = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, tz="Europe/Paris")
+ post = dt.add(microseconds=1)
+
+ assert (post - dt).total_seconds() == 1e-06
+
+ dt = pendulum.datetime(
+ 2013,
+ 10,
+ 27,
+ 2,
+ 59,
+ 59,
+ 999999,
+ tz="Europe/Paris",
+ fold=0,
+ )
+ post = dt.add(microseconds=1)
+
+ assert (post - dt).total_seconds() == 1e-06
diff --git a/tests/datetime/test_fluent_setters.py b/tests/datetime/test_fluent_setters.py
new file mode 100644
index 0000000..cedbd26
--- /dev/null
+++ b/tests/datetime/test_fluent_setters.py
@@ -0,0 +1,181 @@
+from __future__ import annotations
+
+from datetime import datetime
+
+import pendulum
+
+from tests.conftest import assert_datetime
+
+
+def test_fluid_year_setter():
+ d = pendulum.now()
+ new = d.set(year=1995)
+ assert isinstance(new, datetime)
+ assert new.year == 1995
+ assert d.year != new.year
+
+
+def test_fluid_month_setter():
+ d = pendulum.datetime(2016, 7, 2, 0, 41, 20)
+ new = d.set(month=11)
+ assert isinstance(new, datetime)
+ assert new.month == 11
+ assert d.month == 7
+
+
+def test_fluid_day_setter():
+ d = pendulum.datetime(2016, 7, 2, 0, 41, 20)
+ new = d.set(day=9)
+ assert isinstance(new, datetime)
+ assert new.day == 9
+ assert d.day == 2
+
+
+def test_fluid_hour_setter():
+ d = pendulum.datetime(2016, 7, 2, 0, 41, 20)
+ new = d.set(hour=5)
+ assert isinstance(new, datetime)
+ assert new.hour == 5
+ assert d.hour == 0
+
+
+def test_fluid_minute_setter():
+ d = pendulum.datetime(2016, 7, 2, 0, 41, 20)
+ new = d.set(minute=32)
+ assert isinstance(new, datetime)
+ assert new.minute == 32
+ assert d.minute == 41
+
+
+def test_fluid_second_setter():
+ d = pendulum.datetime(2016, 7, 2, 0, 41, 20)
+ new = d.set(second=49)
+ assert isinstance(new, datetime)
+ assert new.second == 49
+ assert d.second == 20
+
+
+def test_fluid_microsecond_setter():
+ d = pendulum.datetime(2016, 7, 2, 0, 41, 20, 123456)
+ new = d.set(microsecond=987654)
+ assert isinstance(new, datetime)
+ assert new.microsecond == 987654
+ assert d.microsecond == 123456
+
+
+def test_fluid_setter_keeps_timezone():
+ d = pendulum.datetime(2016, 7, 2, 0, 41, 20, 123456, tz="Europe/Paris")
+ new = d.set(microsecond=987654)
+ assert_datetime(new, 2016, 7, 2, 0, 41, 20, 987654)
+
+
+def test_fluid_timezone_setter():
+ d = pendulum.datetime(2016, 7, 2, 0, 41, 20)
+ new = d.set(tz="Europe/Paris")
+ assert isinstance(new, datetime)
+ assert new.timezone_name == "Europe/Paris"
+ assert new.tzinfo.name == "Europe/Paris"
+
+
+def test_fluid_on():
+ d = pendulum.datetime(2016, 7, 2, 0, 41, 20)
+ new = d.on(1995, 11, 9)
+ assert isinstance(new, datetime)
+ assert new.year == 1995
+ assert new.month == 11
+ assert new.day == 9
+ assert d.year == 2016
+ assert d.month == 7
+ assert d.day == 2
+
+
+def test_fluid_on_with_transition():
+ d = pendulum.datetime(2013, 3, 31, 0, 0, 0, 0, tz="Europe/Paris")
+ new = d.on(2013, 4, 1)
+ assert isinstance(new, datetime)
+ assert new.year == 2013
+ assert new.month == 4
+ assert new.day == 1
+ assert new.offset == 7200
+ assert d.year == 2013
+ assert d.month == 3
+ assert d.day == 31
+ assert d.offset == 3600
+
+
+def test_fluid_at():
+ d = pendulum.datetime(2016, 7, 2, 0, 41, 20)
+ new = d.at(5, 32, 49, 123456)
+ assert isinstance(new, datetime)
+ assert new.hour == 5
+ assert new.minute == 32
+ assert new.second == 49
+ assert new.microsecond == 123456
+ assert d.hour == 0
+ assert d.minute == 41
+ assert d.second == 20
+ assert d.microsecond == 0
+
+
+def test_fluid_at_partial():
+ d = pendulum.datetime(2016, 7, 2, 0, 41, 20)
+ new = d.at(10)
+
+ assert_datetime(new, 2016, 7, 2, 10, 0, 0, 0)
+
+ new = d.at(10, 30)
+
+ assert_datetime(new, 2016, 7, 2, 10, 30, 0, 0)
+
+ new = d.at(10, 30, 45)
+
+ assert_datetime(new, 2016, 7, 2, 10, 30, 45, 0)
+
+
+def test_fluid_at_with_transition():
+ d = pendulum.datetime(2013, 3, 31, 0, 0, 0, 0, tz="Europe/Paris")
+ new = d.at(2, 30, 0)
+ assert isinstance(new, datetime)
+ assert new.hour == 3
+ assert new.minute == 30
+ assert new.second == 0
+
+
+def test_replace_tzinfo_dst_off():
+ d = pendulum.datetime(2016, 3, 27, 0, 30) # 30 min before DST turning on
+ new = d.replace(tzinfo=pendulum.timezone("Europe/Paris"))
+
+ assert_datetime(new, 2016, 3, 27, 0, 30)
+ assert not new.is_dst()
+ assert new.offset == 3600
+ assert new.timezone_name == "Europe/Paris"
+
+
+def test_replace_tzinfo_dst_transitioning_on():
+ d = pendulum.datetime(2016, 3, 27, 1, 30) # In middle of turning on
+ new = d.replace(tzinfo=pendulum.timezone("Europe/Paris"))
+
+ assert_datetime(new, 2016, 3, 27, 1, 30)
+ assert not new.is_dst()
+ assert new.offset == 3600
+ assert new.timezone_name == "Europe/Paris"
+
+
+def test_replace_tzinfo_dst_on():
+ d = pendulum.datetime(2016, 10, 30, 0, 30) # 30 min before DST turning off
+ new = d.replace(tzinfo=pendulum.timezone("Europe/Paris"))
+
+ assert_datetime(new, 2016, 10, 30, 0, 30)
+ assert new.is_dst()
+ assert new.offset == 7200
+ assert new.timezone_name == "Europe/Paris"
+
+
+def test_replace_tzinfo_dst_transitioning_off():
+ d = pendulum.datetime(2016, 10, 30, 1, 30) # In the middle of turning off
+ new = d.replace(tzinfo=pendulum.timezone("Europe/Paris"))
+
+ assert_datetime(new, 2016, 10, 30, 1, 30)
+ assert new.is_dst()
+ assert new.offset == 7200
+ assert new.timezone_name == "Europe/Paris"
diff --git a/tests/datetime/test_from_format.py b/tests/datetime/test_from_format.py
new file mode 100644
index 0000000..10c4a23
--- /dev/null
+++ b/tests/datetime/test_from_format.py
@@ -0,0 +1,203 @@
+from __future__ import annotations
+
+import pytest
+
+import pendulum
+
+from tests.conftest import assert_datetime
+
+
+def test_from_format_returns_datetime():
+ d = pendulum.from_format("1975-05-21 22:32:11", "YYYY-MM-DD HH:mm:ss")
+ assert_datetime(d, 1975, 5, 21, 22, 32, 11)
+ assert isinstance(d, pendulum.DateTime)
+ assert d.timezone_name == "UTC"
+
+
+def test_from_format_rejects_extra_text():
+ with pytest.raises(ValueError):
+ pendulum.from_format("1975-05-21 22:32:11 extra text", "YYYY-MM-DD HH:mm:ss")
+
+
+def test_from_format_with_timezone_string():
+ d = pendulum.from_format(
+ "1975-05-21 22:32:11", "YYYY-MM-DD HH:mm:ss", tz="Europe/London"
+ )
+ assert_datetime(d, 1975, 5, 21, 22, 32, 11)
+ assert d.timezone_name == "Europe/London"
+
+
+def test_from_format_with_timezone():
+ d = pendulum.from_format(
+ "1975-05-21 22:32:11",
+ "YYYY-MM-DD HH:mm:ss",
+ tz=pendulum.timezone("Europe/London"),
+ )
+ assert_datetime(d, 1975, 5, 21, 22, 32, 11)
+ assert d.timezone_name == "Europe/London"
+
+
+def test_from_format_with_square_bracket_in_timezone():
+ with pytest.raises(ValueError, match="^String does not match format"):
+ pendulum.from_format(
+ "1975-05-21 22:32:11 Eu[rope/London",
+ "YYYY-MM-DD HH:mm:ss z",
+ )
+
+
+def test_from_format_with_escaped_elements():
+ d = pendulum.from_format("1975-05-21T22:32:11+00:00", "YYYY-MM-DD[T]HH:mm:ssZ")
+ assert_datetime(d, 1975, 5, 21, 22, 32, 11)
+ assert d.timezone_name == "+00:00"
+
+
+def test_from_format_with_escaped_elements_valid_tokens():
+ d = pendulum.from_format("1975-05-21T22:32:11.123Z", "YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")
+ assert_datetime(d, 1975, 5, 21, 22, 32, 11)
+ assert d.timezone_name == "UTC"
+
+
+def test_from_format_with_millis():
+ d = pendulum.from_format("1975-05-21 22:32:11.123456", "YYYY-MM-DD HH:mm:ss.SSSSSS")
+ assert_datetime(d, 1975, 5, 21, 22, 32, 11, 123456)
+
+
+def test_from_format_with_padded_day():
+ d = pendulum.from_format("Apr 2 12:00:00 2020 GMT", "MMM DD HH:mm:ss YYYY z")
+ assert_datetime(d, 2020, 4, 2, 12)
+
+
+def test_from_format_with_invalid_padded_day():
+ with pytest.raises(ValueError):
+ pendulum.from_format("Apr 2 12:00:00 2020 GMT", "MMM DD HH:mm:ss YYYY z")
+
+
+@pytest.mark.parametrize(
+ "text,fmt,expected,now",
+ [
+ ("2014-4", "YYYY-Q", "2014-10-01T00:00:00+00:00", None),
+ ("12-02-1999", "MM-DD-YYYY", "1999-12-02T00:00:00+00:00", None),
+ ("12-02-1999", "DD-MM-YYYY", "1999-02-12T00:00:00+00:00", None),
+ ("12/02/1999", "DD/MM/YYYY", "1999-02-12T00:00:00+00:00", None),
+ ("12_02_1999", "DD_MM_YYYY", "1999-02-12T00:00:00+00:00", None),
+ ("12:02:1999", "DD:MM:YYYY", "1999-02-12T00:00:00+00:00", None),
+ ("2-2-99", "D-M-YY", "2099-02-02T00:00:00+00:00", None),
+ ("2-2-99", "D-M-YY", "1999-02-02T00:00:00+00:00", "1990-01-01"),
+ ("99", "YY", "2099-01-01T00:00:00+00:00", None),
+ ("300-1999", "DDD-YYYY", "1999-10-27T00:00:00+00:00", None),
+ ("12-02-1999 2:45:10", "DD-MM-YYYY h:m:s", "1999-02-12T02:45:10+00:00", None),
+ ("12-02-1999 12:45:10", "DD-MM-YYYY h:m:s", "1999-02-12T12:45:10+00:00", None),
+ ("12:00:00", "HH:mm:ss", "2015-11-12T12:00:00+00:00", None),
+ ("12:30:00", "HH:mm:ss", "2015-11-12T12:30:00+00:00", None),
+ ("00:00:00", "HH:mm:ss", "2015-11-12T00:00:00+00:00", None),
+ ("00:30:00 1", "HH:mm:ss S", "2015-11-12T00:30:00.100000+00:00", None),
+ ("00:30:00 12", "HH:mm:ss SS", "2015-11-12T00:30:00.120000+00:00", None),
+ ("00:30:00 123", "HH:mm:ss SSS", "2015-11-12T00:30:00.123000+00:00", None),
+ ("1234567890", "X", "2009-02-13T23:31:30+00:00", None),
+ ("1234567890123", "x", "2009-02-13T23:31:30.123000+00:00", None),
+ ("2016-10-06", "YYYY-MM-DD", "2016-10-06T00:00:00+00:00", None),
+ ("Tuesday", "dddd", "2015-11-10T00:00:00+00:00", None),
+ ("Monday", "dddd", "2018-01-29T00:00:00+00:00", "2018-02-02"),
+ ("Mon", "ddd", "2018-01-29T00:00:00+00:00", "2018-02-02"),
+ ("Mo", "dd", "2018-01-29T00:00:00+00:00", "2018-02-02"),
+ ("0", "d", "2018-02-04T00:00:00+00:00", "2018-02-02"),
+ ("1", "E", "2018-01-29T00:00:00+00:00", "2018-02-02"),
+ ("March", "MMMM", "2018-03-01T00:00:00+00:00", "2018-02-02"),
+ ("Mar", "MMM", "2018-03-01T00:00:00+00:00", "2018-02-02"),
+ (
+ "Thursday 25th December 1975 02:15:16 PM",
+ "dddd Do MMMM YYYY hh:mm:ss A",
+ "1975-12-25T14:15:16+00:00",
+ None,
+ ),
+ (
+ "Thursday 25th December 1975 02:15:16 PM -05:00",
+ "dddd Do MMMM YYYY hh:mm:ss A Z",
+ "1975-12-25T14:15:16-05:00",
+ None,
+ ),
+ (
+ "1975-12-25T14:15:16 America/Guayaquil",
+ "YYYY-MM-DDTHH:mm:ss z",
+ "1975-12-25T14:15:16-05:00",
+ None,
+ ),
+ (
+ "1975-12-25T14:15:16 America/New_York",
+ "YYYY-MM-DDTHH:mm:ss z",
+ "1975-12-25T14:15:16-05:00",
+ None,
+ ),
+ (
+ "1975-12-25T14:15:16 Africa/Porto-Novo",
+ "YYYY-MM-DDTHH:mm:ss z",
+ "1975-12-25T14:15:16+01:00",
+ None,
+ ),
+ (
+ "1975-12-25T14:15:16 Etc/GMT+0",
+ "YYYY-MM-DDTHH:mm:ss z",
+ "1975-12-25T14:15:16+00:00",
+ None,
+ ),
+ (
+ "1975-12-25T14:15:16 W-SU",
+ "YYYY-MM-DDTHH:mm:ss z",
+ "1975-12-25T14:15:16+03:00",
+ None,
+ ),
+ ("190022215", "YYDDDDHHmm", "2019-01-02T22:15:00+00:00", None),
+ ],
+)
+def test_from_format(text, fmt, expected, now):
+ if now is None:
+ now = pendulum.datetime(2015, 11, 12)
+ else:
+ now = pendulum.parse(now)
+
+ with pendulum.travel_to(now, freeze=True):
+ assert pendulum.from_format(text, fmt).isoformat() == expected
+
+
+@pytest.mark.parametrize(
+ "text,fmt,expected",
+ [
+ ("lundi", "dddd", "2018-01-29T00:00:00+00:00"),
+ ("lun.", "ddd", "2018-01-29T00:00:00+00:00"),
+ ("lu", "dd", "2018-01-29T00:00:00+00:00"),
+ ("mars", "MMMM", "2018-03-01T00:00:00+00:00"),
+ ("mars", "MMM", "2018-03-01T00:00:00+00:00"),
+ ],
+)
+def test_from_format_with_locale(text, fmt, expected):
+ now = pendulum.datetime(2018, 2, 2)
+
+ with pendulum.travel_to(now, freeze=True):
+ formatted = pendulum.from_format(text, fmt, locale="fr").isoformat()
+ assert formatted == expected
+
+
+@pytest.mark.parametrize(
+ "text,fmt,locale",
+ [
+ ("23:00", "hh:mm", "en"),
+ ("23:00 am", "HH:mm a", "en"),
+ ("invalid", "dddd", "en"),
+ ("invalid", "ddd", "en"),
+ ("invalid", "dd", "en"),
+ ("invalid", "MMMM", "en"),
+ ("invalid", "MMM", "en"),
+ ],
+)
+def test_from_format_error(text, fmt, locale):
+ now = pendulum.datetime(2018, 2, 2)
+
+ with pendulum.travel_to(now, freeze=True), pytest.raises(ValueError):
+ pendulum.from_format(text, fmt, locale=locale)
+
+
+def test_strptime():
+ d = pendulum.DateTime.strptime("1975-05-21 22:32:11", "%Y-%m-%d %H:%M:%S")
+ assert_datetime(d, 1975, 5, 21, 22, 32, 11)
+ assert isinstance(d, pendulum.DateTime)
+ assert d.timezone_name == "UTC"
diff --git a/tests/datetime/test_getters.py b/tests/datetime/test_getters.py
new file mode 100644
index 0000000..5074623
--- /dev/null
+++ b/tests/datetime/test_getters.py
@@ -0,0 +1,248 @@
+from __future__ import annotations
+
+import struct
+
+import pytest
+
+import pendulum
+
+from pendulum import DateTime
+from pendulum.tz import timezone
+from tests.conftest import assert_date
+from tests.conftest import assert_time
+
+
+def test_year():
+ d = pendulum.datetime(1234, 5, 6, 7, 8, 9)
+ assert d.year == 1234
+
+
+def test_month():
+ d = pendulum.datetime(1234, 5, 6, 7, 8, 9)
+ assert d.month == 5
+
+
+def test_day():
+ d = pendulum.datetime(1234, 5, 6, 7, 8, 9)
+ assert d.day == 6
+
+
+def test_hour():
+ d = pendulum.datetime(1234, 5, 6, 7, 8, 9)
+ assert d.hour == 7
+
+
+def test_minute():
+ d = pendulum.datetime(1234, 5, 6, 7, 8, 9)
+ assert d.minute == 8
+
+
+def test_second():
+ d = pendulum.datetime(1234, 5, 6, 7, 8, 9)
+ assert d.second == 9
+
+
+def test_microsecond():
+ d = pendulum.datetime(1234, 5, 6, 7, 8, 9)
+ assert d.microsecond == 0
+
+ d = pendulum.datetime(1234, 5, 6, 7, 8, 9, 101112)
+ assert d.microsecond == 101112
+
+
+def test_tzinfo():
+ d = pendulum.now()
+ assert d.tzinfo.name == timezone("America/Toronto").name
+
+
+def test_day_of_week():
+ d = pendulum.datetime(2012, 5, 7, 7, 8, 9)
+ assert d.day_of_week == pendulum.MONDAY
+
+
+def test_day_of_year():
+ d = pendulum.datetime(2012, 5, 7)
+ assert d.day_of_year == 128
+
+
+def test_days_in_month():
+ d = pendulum.datetime(2012, 5, 7)
+ assert d.days_in_month == 31
+
+
+def test_timestamp():
+ d = pendulum.datetime(1970, 1, 1, 0, 0, 0)
+ assert d.timestamp() == 0
+ assert d.add(minutes=1, microseconds=123456).timestamp() == 60.123456
+
+
+def test_float_timestamp():
+ d = pendulum.datetime(1970, 1, 1, 0, 0, 0, 123456)
+ assert d.float_timestamp == 0.123456
+
+
+def test_int_timestamp():
+ d = pendulum.datetime(1970, 1, 1, 0, 0, 0)
+ assert d.int_timestamp == 0
+ assert d.add(minutes=1, microseconds=123456).int_timestamp == 60
+
+
+@pytest.mark.skipif(
+ struct.calcsize("P") * 8 == 32, reason="Test only available for 64bit systems"
+)
+def test_int_timestamp_accuracy():
+ d = pendulum.datetime(3000, 10, 1, 12, 23, 10, 999999)
+
+ assert d.int_timestamp == 32527311790
+
+
+def test_timestamp_with_transition():
+ d_pre = pendulum.datetime(2012, 10, 28, 2, 0, tz="Europe/Warsaw", fold=0)
+ d_post = pendulum.datetime(2012, 10, 28, 2, 0, tz="Europe/Warsaw", fold=1)
+
+ # the difference between the timestamps before and after is equal to one hour
+ assert d_post.timestamp() - d_pre.timestamp() == pendulum.SECONDS_PER_HOUR
+ assert d_post.float_timestamp - d_pre.float_timestamp == (pendulum.SECONDS_PER_HOUR)
+ assert d_post.int_timestamp - d_pre.int_timestamp == pendulum.SECONDS_PER_HOUR
+
+
+def test_age():
+ d = pendulum.now()
+ assert d.age == 0
+ assert d.add(years=1).age == -1
+ assert d.subtract(years=1).age == 1
+
+
+def test_local():
+ assert pendulum.datetime(2012, 1, 1, tz="America/Toronto").is_local()
+ assert pendulum.datetime(2012, 1, 1, tz="America/New_York").is_local()
+ assert not pendulum.datetime(2012, 1, 1, tz="UTC").is_local()
+ assert not pendulum.datetime(2012, 1, 1, tz="Europe/London").is_local()
+
+
+def test_utc():
+ assert not pendulum.datetime(2012, 1, 1, tz="America/Toronto").is_utc()
+ assert not pendulum.datetime(2012, 1, 1, tz="Europe/Paris").is_utc()
+ assert pendulum.datetime(2012, 1, 1, tz="UTC").is_utc()
+ assert pendulum.datetime(2012, 1, 1, tz=0).is_utc()
+ assert not pendulum.datetime(2012, 1, 1, tz=5).is_utc()
+ # There is no time difference between Greenwich Mean Time and Coordinated Universal Time
+ assert pendulum.datetime(2012, 1, 1, tz="GMT").is_utc()
+
+
+def test_is_dst():
+ assert not pendulum.datetime(2012, 1, 1, tz="America/Toronto").is_dst()
+ assert pendulum.datetime(2012, 7, 1, tz="America/Toronto").is_dst()
+
+
+def test_offset_with_dst():
+ assert pendulum.datetime(2012, 1, 1, tz="America/Toronto").offset == -18000
+
+
+def test_offset_no_dst():
+ assert pendulum.datetime(2012, 6, 1, tz="America/Toronto").offset == -14400
+
+
+def test_offset_for_gmt():
+ assert pendulum.datetime(2012, 6, 1, tz="GMT").offset == 0
+
+
+def test_offset_hours_with_dst():
+ assert pendulum.datetime(2012, 1, 1, tz="America/Toronto").offset_hours == -5
+
+
+def test_offset_hours_no_dst():
+ assert pendulum.datetime(2012, 6, 1, tz="America/Toronto").offset_hours == -4
+
+
+def test_offset_hours_for_gmt():
+ assert pendulum.datetime(2012, 6, 1, tz="GMT").offset_hours == 0
+
+
+def test_offset_hours_float():
+ assert pendulum.datetime(2012, 6, 1, tz=9.5).offset_hours == 9.5
+
+
+def test_is_leap_year():
+ assert pendulum.datetime(2012, 1, 1).is_leap_year()
+ assert not pendulum.datetime(2011, 1, 1).is_leap_year()
+
+
+def test_is_long_year():
+ assert pendulum.datetime(2015, 1, 1).is_long_year()
+ assert not pendulum.datetime(2016, 1, 1).is_long_year()
+
+
+def test_week_of_month():
+ assert pendulum.datetime(2012, 9, 30).week_of_month == 5
+ assert pendulum.datetime(2012, 9, 28).week_of_month == 5
+ assert pendulum.datetime(2012, 9, 20).week_of_month == 4
+ assert pendulum.datetime(2012, 9, 8).week_of_month == 2
+ assert pendulum.datetime(2012, 9, 1).week_of_month == 1
+ assert pendulum.datetime(2020, 1, 1).week_of_month == 1
+ assert pendulum.datetime(2020, 1, 7).week_of_month == 2
+ assert pendulum.datetime(2020, 1, 14).week_of_month == 3
+
+
+def test_week_of_year_first_week():
+ assert pendulum.datetime(2012, 1, 1).week_of_year == 52
+ assert pendulum.datetime(2012, 1, 2).week_of_year == 1
+
+
+def test_week_of_year_last_week():
+ assert pendulum.datetime(2012, 12, 30).week_of_year == 52
+ assert pendulum.datetime(2012, 12, 31).week_of_year == 1
+
+
+def test_timezone():
+ d = pendulum.datetime(2000, 1, 1, tz="America/Toronto")
+ assert d.timezone.name == "America/Toronto"
+
+ d = pendulum.datetime(2000, 1, 1, tz=-5)
+ assert d.timezone.name == "-05:00"
+
+
+def test_tz():
+ d = pendulum.datetime(2000, 1, 1, tz="America/Toronto")
+ assert d.tz.name == "America/Toronto"
+
+ d = pendulum.datetime(2000, 1, 1, tz=-5)
+ assert d.tz.name == "-05:00"
+
+
+def test_timezone_name():
+ d = pendulum.datetime(2000, 1, 1, tz="America/Toronto")
+ assert d.timezone_name == "America/Toronto"
+
+ d = pendulum.datetime(2000, 1, 1, tz=-5)
+ assert d.timezone_name == "-05:00"
+
+
+def test_is_future():
+ with pendulum.travel_to(DateTime(2000, 1, 1)):
+ d = pendulum.now()
+ assert not d.is_future()
+ d = d.add(days=1)
+ assert d.is_future()
+
+
+def test_is_past():
+ with pendulum.travel_to(DateTime(2000, 1, 1), freeze=True):
+ d = pendulum.now()
+ assert not d.is_past()
+ d = d.subtract(days=1)
+ assert d.is_past()
+
+
+def test_date():
+ dt = pendulum.datetime(2016, 10, 20, 10, 40, 34, 123456)
+ d = dt.date()
+ assert isinstance(d, pendulum.Date)
+ assert_date(d, 2016, 10, 20)
+
+
+def test_time():
+ dt = pendulum.datetime(2016, 10, 20, 10, 40, 34, 123456)
+ t = dt.time()
+ assert isinstance(t, pendulum.Time)
+ assert_time(t, 10, 40, 34, 123456)
diff --git a/tests/datetime/test_naive.py b/tests/datetime/test_naive.py
new file mode 100644
index 0000000..95bd2d5
--- /dev/null
+++ b/tests/datetime/test_naive.py
@@ -0,0 +1,78 @@
+from __future__ import annotations
+
+import pendulum
+
+from tests.conftest import assert_datetime
+
+
+def test_naive():
+ dt = pendulum.naive(2018, 2, 2, 12, 34, 56, 123456)
+
+ assert_datetime(dt, 2018, 2, 2, 12, 34, 56, 123456)
+ assert dt.tzinfo is None
+ assert dt.timezone is None
+ assert dt.timezone_name is None
+
+
+def test_naive_add():
+ dt = pendulum.naive(2013, 3, 31, 1, 30)
+ new = dt.add(hours=1)
+
+ assert_datetime(new, 2013, 3, 31, 2, 30)
+
+
+def test_naive_subtract():
+ dt = pendulum.naive(2013, 3, 31, 1, 30)
+ new = dt.subtract(hours=1)
+
+ assert_datetime(new, 2013, 3, 31, 0, 30)
+
+
+def test_naive_in_timezone():
+ dt = pendulum.naive(2013, 3, 31, 1, 30)
+ new = dt.in_timezone("Europe/Paris")
+
+ assert_datetime(new, 2013, 3, 31, 1, 30)
+ assert new.timezone_name == "Europe/Paris"
+
+
+def test_naive_in_timezone_dst():
+ dt = pendulum.naive(2013, 3, 31, 2, 30)
+ new = dt.in_timezone("Europe/Paris")
+
+ assert_datetime(new, 2013, 3, 31, 3, 30)
+ assert new.timezone_name == "Europe/Paris"
+
+
+def test_add():
+ dt = pendulum.naive(2013, 3, 31, 2, 30)
+ new = dt.add(days=3)
+
+ assert_datetime(new, 2013, 4, 3, 2, 30)
+
+
+def test_subtract():
+ dt = pendulum.naive(2013, 3, 31, 2, 30)
+ new = dt.subtract(days=3)
+
+ assert_datetime(new, 2013, 3, 28, 2, 30)
+
+
+def test_to_strings():
+ dt = pendulum.naive(2013, 3, 31, 2, 30)
+
+ assert dt.isoformat() == "2013-03-31T02:30:00"
+ assert dt.to_iso8601_string() == "2013-03-31T02:30:00"
+ assert dt.to_rfc3339_string() == "2013-03-31T02:30:00"
+ assert dt.to_atom_string() == "2013-03-31T02:30:00"
+ assert dt.to_cookie_string() == "Sunday, 31-Mar-2013 02:30:00 "
+
+
+def test_naive_method():
+ dt = pendulum.datetime(2018, 2, 2, 12, 34, 56, 123456)
+ dt = dt.naive()
+
+ assert_datetime(dt, 2018, 2, 2, 12, 34, 56, 123456)
+ assert dt.tzinfo is None
+ assert dt.timezone is None
+ assert dt.timezone_name is None
diff --git a/tests/datetime/test_replace.py b/tests/datetime/test_replace.py
new file mode 100644
index 0000000..694ef7b
--- /dev/null
+++ b/tests/datetime/test_replace.py
@@ -0,0 +1,61 @@
+from __future__ import annotations
+
+import pendulum
+
+from tests.conftest import assert_datetime
+
+
+def test_replace_tzinfo_dst_off():
+ utc = pendulum.datetime(2016, 3, 27, 0, 30) # 30 min before DST turning on
+ in_paris = utc.in_tz("Europe/Paris")
+
+ assert_datetime(in_paris, 2016, 3, 27, 1, 30, 0)
+
+ in_paris = in_paris.replace(second=1)
+
+ assert_datetime(in_paris, 2016, 3, 27, 1, 30, 1)
+ assert not in_paris.is_dst()
+ assert in_paris.offset == 3600
+ assert in_paris.timezone_name == "Europe/Paris"
+
+
+def test_replace_tzinfo_dst_transitioning_on():
+ utc = pendulum.datetime(2016, 3, 27, 1, 30) # In middle of turning on
+ in_paris = utc.in_tz("Europe/Paris")
+
+ assert_datetime(in_paris, 2016, 3, 27, 3, 30, 0)
+
+ in_paris = in_paris.replace(second=1)
+
+ assert_datetime(in_paris, 2016, 3, 27, 3, 30, 1)
+ assert in_paris.is_dst()
+ assert in_paris.offset == 7200
+ assert in_paris.timezone_name == "Europe/Paris"
+
+
+def test_replace_tzinfo_dst_on():
+ utc = pendulum.datetime(2016, 10, 30, 0, 30) # 30 min before DST turning off
+ in_paris = utc.in_tz("Europe/Paris")
+
+ assert_datetime(in_paris, 2016, 10, 30, 2, 30, 0)
+
+ in_paris = in_paris.replace(second=1)
+
+ assert_datetime(in_paris, 2016, 10, 30, 2, 30, 1)
+ assert in_paris.is_dst()
+ assert in_paris.offset == 7200
+ assert in_paris.timezone_name == "Europe/Paris"
+
+
+def test_replace_tzinfo_dst_transitioning_off():
+ utc = pendulum.datetime(2016, 10, 30, 1, 30) # In the middle of turning off
+ in_paris = utc.in_tz("Europe/Paris")
+
+ assert_datetime(in_paris, 2016, 10, 30, 2, 30, 0)
+
+ in_paris = in_paris.replace(second=1)
+
+ assert_datetime(in_paris, 2016, 10, 30, 2, 30, 1)
+ assert not in_paris.is_dst()
+ assert in_paris.offset == 3600
+ assert in_paris.timezone_name == "Europe/Paris"
diff --git a/tests/datetime/test_start_end_of.py b/tests/datetime/test_start_end_of.py
new file mode 100644
index 0000000..597dbeb
--- /dev/null
+++ b/tests/datetime/test_start_end_of.py
@@ -0,0 +1,285 @@
+from __future__ import annotations
+
+import pytest
+
+import pendulum
+
+from tests.conftest import assert_datetime
+
+
+def test_start_of_second():
+ d = pendulum.now()
+ new = d.start_of("second")
+ assert isinstance(new, pendulum.DateTime)
+ assert_datetime(new, d.year, d.month, d.day, d.hour, d.minute, d.second, 0)
+
+
+def test_end_of_second():
+ d = pendulum.now()
+ new = d.end_of("second")
+ assert isinstance(new, pendulum.DateTime)
+ assert_datetime(new, d.year, d.month, d.day, d.hour, d.minute, d.second, 999999)
+
+
+def test_start_of_minute():
+ d = pendulum.now()
+ new = d.start_of("minute")
+ assert isinstance(new, pendulum.DateTime)
+ assert_datetime(new, d.year, d.month, d.day, d.hour, d.minute, 0, 0)
+
+
+def test_end_of_minute():
+ d = pendulum.now()
+ new = d.end_of("minute")
+ assert isinstance(new, pendulum.DateTime)
+ assert_datetime(new, d.year, d.month, d.day, d.hour, d.minute, 59, 999999)
+
+
+def test_start_of_hour():
+ d = pendulum.now()
+ new = d.start_of("hour")
+ assert isinstance(new, pendulum.DateTime)
+ assert_datetime(new, d.year, d.month, d.day, d.hour, 0, 0, 0)
+
+
+def test_end_of_hour():
+ d = pendulum.now()
+ new = d.end_of("hour")
+ assert isinstance(new, pendulum.DateTime)
+ assert_datetime(new, d.year, d.month, d.day, d.hour, 59, 59, 999999)
+
+
+def test_start_of_day():
+ d = pendulum.now()
+ new = d.start_of("day")
+ assert isinstance(new, pendulum.DateTime)
+ assert_datetime(new, d.year, d.month, d.day, 0, 0, 0, 0)
+
+
+def test_end_of_day():
+ d = pendulum.now()
+ new = d.end_of("day")
+ assert isinstance(new, pendulum.DateTime)
+ assert_datetime(new, d.year, d.month, d.day, 23, 59, 59, 999999)
+
+
+def test_start_of_month_is_fluid():
+ d = pendulum.now()
+ assert isinstance(d.start_of("month"), pendulum.DateTime)
+
+
+def test_start_of_month_from_now():
+ d = pendulum.now()
+ new = d.start_of("month")
+ assert_datetime(new, d.year, d.month, 1, 0, 0, 0, 0)
+
+
+def test_start_of_month_from_last_day():
+ d = pendulum.datetime(2000, 1, 31, 2, 3, 4)
+ new = d.start_of("month")
+ assert_datetime(new, 2000, 1, 1, 0, 0, 0, 0)
+
+
+def test_start_of_year_is_fluid():
+ d = pendulum.now()
+ new = d.start_of("year")
+ assert isinstance(new, pendulum.DateTime)
+
+
+def test_start_of_year_from_now():
+ d = pendulum.now()
+ new = d.start_of("year")
+ assert_datetime(new, d.year, 1, 1, 0, 0, 0, 0)
+
+
+def test_start_of_year_from_first_day():
+ d = pendulum.datetime(2000, 1, 1, 1, 1, 1)
+ new = d.start_of("year")
+ assert_datetime(new, 2000, 1, 1, 0, 0, 0, 0)
+
+
+def test_start_of_year_from_last_day():
+ d = pendulum.datetime(2000, 12, 31, 23, 59, 59)
+ new = d.start_of("year")
+ assert_datetime(new, 2000, 1, 1, 0, 0, 0, 0)
+
+
+def test_end_of_month_is_fluid():
+ d = pendulum.now()
+ assert isinstance(d.end_of("month"), pendulum.DateTime)
+
+
+def test_end_of_month():
+ d = pendulum.datetime(2000, 1, 1, 2, 3, 4).end_of("month")
+ new = d.end_of("month")
+ assert_datetime(new, 2000, 1, 31, 23, 59, 59)
+
+
+def test_end_of_month_from_last_day():
+ d = pendulum.datetime(2000, 1, 31, 2, 3, 4)
+ new = d.end_of("month")
+ assert_datetime(new, 2000, 1, 31, 23, 59, 59)
+
+
+def test_end_of_year_is_fluid():
+ d = pendulum.now()
+ assert isinstance(d.end_of("year"), pendulum.DateTime)
+
+
+def test_end_of_year_from_now():
+ d = pendulum.now().end_of("year")
+ new = d.end_of("year")
+ assert_datetime(new, d.year, 12, 31, 23, 59, 59, 999999)
+
+
+def test_end_of_year_from_first_day():
+ d = pendulum.datetime(2000, 1, 1, 1, 1, 1)
+ new = d.end_of("year")
+ assert_datetime(new, 2000, 12, 31, 23, 59, 59, 999999)
+
+
+def test_end_of_year_from_last_day():
+ d = pendulum.datetime(2000, 12, 31, 23, 59, 59, 999999)
+ new = d.end_of("year")
+ assert_datetime(new, 2000, 12, 31, 23, 59, 59, 999999)
+
+
+def test_start_of_decade_is_fluid():
+ d = pendulum.now()
+ assert isinstance(d.start_of("decade"), pendulum.DateTime)
+
+
+def test_start_of_decade_from_now():
+ d = pendulum.now()
+ new = d.start_of("decade")
+ assert_datetime(new, d.year - d.year % 10, 1, 1, 0, 0, 0, 0)
+
+
+def test_start_of_decade_from_first_day():
+ d = pendulum.datetime(2000, 1, 1, 1, 1, 1)
+ new = d.start_of("decade")
+ assert_datetime(new, 2000, 1, 1, 0, 0, 0, 0)
+
+
+def test_start_of_decade_from_last_day():
+ d = pendulum.datetime(2009, 12, 31, 23, 59, 59)
+ new = d.start_of("decade")
+ assert_datetime(new, 2000, 1, 1, 0, 0, 0, 0)
+
+
+def test_end_of_decade_is_fluid():
+ d = pendulum.now()
+ assert isinstance(d.end_of("decade"), pendulum.DateTime)
+
+
+def test_end_of_decade_from_now():
+ d = pendulum.now()
+ new = d.end_of("decade")
+ assert_datetime(new, d.year - d.year % 10 + 9, 12, 31, 23, 59, 59, 999999)
+
+
+def test_end_of_decade_from_first_day():
+ d = pendulum.datetime(2000, 1, 1, 1, 1, 1)
+ new = d.end_of("decade")
+ assert_datetime(new, 2009, 12, 31, 23, 59, 59, 999999)
+
+
+def test_end_of_decade_from_last_day():
+ d = pendulum.datetime(2009, 12, 31, 23, 59, 59, 999999)
+ new = d.end_of("decade")
+ assert_datetime(new, 2009, 12, 31, 23, 59, 59, 999999)
+
+
+def test_start_of_century_is_fluid():
+ d = pendulum.now()
+ assert isinstance(d.start_of("century"), pendulum.DateTime)
+
+
+def test_start_of_century_from_now():
+ d = pendulum.now()
+ new = d.start_of("century")
+ assert_datetime(new, d.year - d.year % 100 + 1, 1, 1, 0, 0, 0, 0)
+
+
+def test_start_of_century_from_first_day():
+ d = pendulum.datetime(2001, 1, 1, 1, 1, 1)
+ new = d.start_of("century")
+ assert_datetime(new, 2001, 1, 1, 0, 0, 0, 0)
+
+
+def test_start_of_century_from_last_day():
+ d = pendulum.datetime(2100, 12, 31, 23, 59, 59)
+ new = d.start_of("century")
+ assert_datetime(new, 2001, 1, 1, 0, 0, 0, 0)
+
+
+def test_end_of_century_is_fluid():
+ d = pendulum.now()
+ assert isinstance(d.end_of("century"), pendulum.DateTime)
+
+
+def test_end_of_century_from_now():
+ now = pendulum.now()
+ d = now.end_of("century")
+ assert_datetime(d, now.year - now.year % 100 + 100, 12, 31, 23, 59, 59, 999999)
+
+
+def test_end_of_century_from_first_day():
+ d = pendulum.datetime(2001, 1, 1, 1, 1, 1)
+ new = d.end_of("century")
+ assert_datetime(new, 2100, 12, 31, 23, 59, 59, 999999)
+
+
+def test_end_of_century_from_last_day():
+ d = pendulum.datetime(2100, 12, 31, 23, 59, 59, 999999)
+ new = d.end_of("century")
+ assert_datetime(new, 2100, 12, 31, 23, 59, 59, 999999)
+
+
+def test_average_is_fluid():
+ d = pendulum.now().average()
+ assert isinstance(d, pendulum.DateTime)
+
+
+def test_average_from_same():
+ d1 = pendulum.datetime(2000, 1, 31, 2, 3, 4)
+ d2 = pendulum.datetime(2000, 1, 31, 2, 3, 4).average(d1)
+ assert_datetime(d2, 2000, 1, 31, 2, 3, 4)
+
+
+def test_average_from_greater():
+ d1 = pendulum.datetime(2000, 1, 1, 1, 1, 1, tz="local")
+ d2 = pendulum.datetime(2009, 12, 31, 23, 59, 59, tz="local").average(d1)
+ assert_datetime(d2, 2004, 12, 31, 12, 30, 30)
+
+
+def test_average_from_lower():
+ d1 = pendulum.datetime(2009, 12, 31, 23, 59, 59, tz="local")
+ d2 = pendulum.datetime(2000, 1, 1, 1, 1, 1, tz="local").average(d1)
+ assert_datetime(d2, 2004, 12, 31, 12, 30, 30)
+
+
+def start_of_with_invalid_unit():
+ with pytest.raises(ValueError):
+ pendulum.now().start_of("invalid")
+
+
+def end_of_with_invalid_unit():
+ with pytest.raises(ValueError):
+ pendulum.now().end_of("invalid")
+
+
+def test_start_of_with_transition():
+ d = pendulum.datetime(2013, 10, 27, 23, 59, 59, tz="Europe/Paris")
+ assert d.offset == 3600
+ assert d.start_of("month").offset == 7200
+ assert d.start_of("day").offset == 7200
+ assert d.start_of("year").offset == 3600
+
+
+def test_end_of_with_transition():
+ d = pendulum.datetime(2013, 3, 31, tz="Europe/Paris")
+ assert d.offset == 3600
+ assert d.end_of("month").offset == 7200
+ assert d.end_of("day").offset == 7200
+ assert d.end_of("year").offset == 3600
diff --git a/tests/datetime/test_strings.py b/tests/datetime/test_strings.py
new file mode 100644
index 0000000..0de340d
--- /dev/null
+++ b/tests/datetime/test_strings.py
@@ -0,0 +1,141 @@
+from __future__ import annotations
+
+import pytest
+
+import pendulum
+
+
+def test_to_string():
+ d = pendulum.datetime(1975, 12, 25, 0, 0, 0, 0, tz="local")
+ assert str(d) == d.to_iso8601_string()
+ d = pendulum.datetime(1975, 12, 25, 0, 0, 0, 123456, tz="local")
+ assert str(d) == d.to_iso8601_string()
+
+
+def test_to_date_string():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16)
+
+ assert d.to_date_string() == "1975-12-25"
+
+
+def test_to_formatted_date_string():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16)
+
+ assert d.to_formatted_date_string() == "Dec 25, 1975"
+
+
+def test_to_timestring():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16)
+
+ assert d.to_time_string() == "14:15:16"
+
+
+def test_to_atom_string():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local")
+ assert d.to_atom_string() == "1975-12-25T14:15:16-05:00"
+
+
+def test_to_cookie_string():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local")
+ assert d.to_cookie_string() == "Thursday, 25-Dec-1975 14:15:16 EST"
+
+
+def test_to_iso8601_string():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local")
+ assert d.to_iso8601_string() == "1975-12-25T14:15:16-05:00"
+
+
+def test_to_iso8601_string_utc():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16)
+ assert d.to_iso8601_string() == "1975-12-25T14:15:16Z"
+
+
+def test_to_iso8601_extended_string():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, 123456, tz="local")
+ assert d.to_iso8601_string() == "1975-12-25T14:15:16.123456-05:00"
+
+
+def test_to_rfc822_string():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local")
+ assert d.to_rfc822_string() == "Thu, 25 Dec 75 14:15:16 -0500"
+
+
+def test_to_rfc850_string():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local")
+ assert d.to_rfc850_string() == "Thursday, 25-Dec-75 14:15:16 EST"
+
+
+def test_to_rfc1036_string():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local")
+ assert d.to_rfc1036_string() == "Thu, 25 Dec 75 14:15:16 -0500"
+
+
+def test_to_rfc1123_string():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local")
+ assert d.to_rfc1123_string() == "Thu, 25 Dec 1975 14:15:16 -0500"
+
+
+def test_to_rfc2822_string():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local")
+ assert d.to_rfc2822_string() == "Thu, 25 Dec 1975 14:15:16 -0500"
+
+
+def test_to_rfc3339_string():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local")
+ assert d.to_rfc3339_string() == "1975-12-25T14:15:16-05:00"
+
+
+def test_to_rfc3339_extended_string():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, 123456, tz="local")
+ assert d.to_rfc3339_string() == "1975-12-25T14:15:16.123456-05:00"
+
+
+def test_to_rss_string():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local")
+ assert d.to_rss_string() == "Thu, 25 Dec 1975 14:15:16 -0500"
+
+
+def test_to_w3c_string():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local")
+ assert d.to_w3c_string() == "1975-12-25T14:15:16-05:00"
+
+
+def test_to_string_invalid():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local")
+
+ with pytest.raises(ValueError):
+ d._to_string("invalid")
+
+
+def test_repr():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local")
+ expected = f"DateTime(1975, 12, 25, 14, 15, 16, tzinfo={repr(d.tzinfo)})"
+ assert repr(d) == expected
+
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, 123456, tz="local")
+ expected = f"DateTime(1975, 12, 25, 14, 15, 16, 123456, tzinfo={repr(d.tzinfo)})"
+ assert repr(d) == expected
+
+
+def test_format_with_locale():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local")
+ expected = "jeudi 25e jour de décembre 1975 02:15:16 PM -05:00"
+ assert d.format("dddd Do [jour de] MMMM YYYY hh:mm:ss A Z", locale="fr") == expected
+
+
+def test_strftime():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local")
+ assert d.strftime("%d") == "25"
+
+
+def test_for_json():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local")
+ assert d.for_json() == "1975-12-25T14:15:16-05:00"
+
+
+def test_format():
+ d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="Europe/Paris")
+ assert f"{d}" == "1975-12-25T14:15:16+01:00"
+ assert f"{d:YYYY}" == "1975"
+ assert f"{d:%Y}" == "1975"
+ assert f"{d:%H:%M %d.%m.%Y}" == "14:15 25.12.1975"
diff --git a/tests/datetime/test_sub.py b/tests/datetime/test_sub.py
new file mode 100644
index 0000000..1e51977
--- /dev/null
+++ b/tests/datetime/test_sub.py
@@ -0,0 +1,251 @@
+from __future__ import annotations
+
+from datetime import timedelta
+
+import pytest
+
+import pendulum
+
+from tests.conftest import assert_datetime
+
+
+def test_sub_years_positive():
+ assert pendulum.datetime(1975, 1, 1).subtract(years=1).year == 1974
+
+
+def test_sub_years_zero():
+ assert pendulum.datetime(1975, 1, 1).subtract(years=0).year == 1975
+
+
+def test_sub_years_negative():
+ assert pendulum.datetime(1975, 1, 1).subtract(years=-1).year == 1976
+
+
+def test_sub_months_positive():
+ assert pendulum.datetime(1975, 12, 1).subtract(months=1).month == 11
+
+
+def test_sub_months_zero():
+ assert pendulum.datetime(1975, 12, 1).subtract(months=0).month == 12
+
+
+def test_sub_months_negative():
+ assert pendulum.datetime(1975, 12, 1).subtract(months=-1).month == 1
+
+
+def test_sub_days_positive():
+ assert pendulum.datetime(1975, 5, 31).subtract(days=1).day == 30
+
+
+def test_sub_days_zero():
+ assert pendulum.datetime(1975, 5, 31).subtract(days=0).day == 31
+
+
+def test_sub_days_negative():
+ assert pendulum.datetime(1975, 5, 31).subtract(days=-1).day == 1
+
+
+def test_sub_weeks_positive():
+ assert pendulum.datetime(1975, 5, 21).subtract(weeks=1).day == 14
+
+
+def test_sub_weeks_zero():
+ assert pendulum.datetime(1975, 5, 21).subtract(weeks=0).day == 21
+
+
+def test_sub_weeks_negative():
+ assert pendulum.datetime(1975, 5, 21).subtract(weeks=-1).day == 28
+
+
+def test_sub_hours_positive():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).subtract(hours=1).hour == 23
+
+
+def test_sub_hours_zero():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).subtract(hours=0).hour == 0
+
+
+def test_sub_hours_negative():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).subtract(hours=-1).hour == 1
+
+
+def test_sub_minutes_positive():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).subtract(minutes=1).minute == 59
+
+
+def test_sub_minutes_zero():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).subtract(minutes=0).minute == 0
+
+
+def test_sub_minutes_negative():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).subtract(minutes=-1).minute == 1
+
+
+def test_sub_seconds_positive():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).subtract(seconds=1).second == 59
+
+
+def test_sub_seconds_zero():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).subtract(seconds=0).second == 0
+
+
+def test_sub_seconds_negative():
+ assert pendulum.datetime(1975, 5, 21, 0, 0, 0).subtract(seconds=-1).second == 1
+
+
+def test_subtract_timedelta():
+ delta = timedelta(days=6, seconds=16, microseconds=654321)
+ d = pendulum.datetime(2015, 3, 14, 3, 12, 15, 777777)
+
+ d = d - delta
+ assert d.day == 8
+ assert d.minute == 11
+ assert d.second == 59
+ assert d.microsecond == 123456
+
+
+def test_subtract_duration():
+ duration = pendulum.duration(
+ years=2, months=3, days=6, seconds=16, microseconds=654321
+ )
+ d = pendulum.datetime(2015, 3, 14, 3, 12, 15, 777777)
+
+ d = d - duration
+ assert d.year == 2012
+ assert d.month == 12
+ assert d.day == 8
+ assert d.hour == 3
+ assert d.minute == 11
+ assert d.second == 59
+ assert d.microsecond == 123456
+
+
+def test_subtract_time_to_new_transition_skipped():
+ dt = pendulum.datetime(2013, 3, 31, 3, 0, 0, 0, tz="Europe/Paris")
+
+ assert_datetime(dt, 2013, 3, 31, 3, 0, 0, 0)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 7200
+ assert dt.is_dst()
+
+ dt = dt.subtract(microseconds=1)
+
+ assert_datetime(dt, 2013, 3, 31, 1, 59, 59, 999999)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 3600
+ assert not dt.is_dst()
+
+ dt = pendulum.datetime(2013, 3, 10, 3, 0, 0, 0, tz="America/New_York")
+
+ assert_datetime(dt, 2013, 3, 10, 3, 0, 0, 0)
+ assert dt.timezone_name == "America/New_York"
+ assert dt.offset == -4 * 3600
+ assert dt.is_dst()
+
+ dt = dt.subtract(microseconds=1)
+
+ assert_datetime(dt, 2013, 3, 10, 1, 59, 59, 999999)
+ assert dt.timezone_name == "America/New_York"
+ assert dt.offset == -5 * 3600
+ assert not dt.is_dst()
+
+ dt = pendulum.datetime(1957, 4, 28, 3, 0, 0, 0, tz="America/New_York")
+
+ assert_datetime(dt, 1957, 4, 28, 3, 0, 0, 0)
+ assert dt.timezone_name == "America/New_York"
+ assert dt.offset == -4 * 3600
+ assert dt.is_dst()
+
+ dt = dt.subtract(microseconds=1)
+
+ assert_datetime(dt, 1957, 4, 28, 1, 59, 59, 999999)
+ assert dt.timezone_name == "America/New_York"
+ assert dt.offset == -5 * 3600
+ assert not dt.is_dst()
+
+
+def test_subtract_time_to_new_transition_skipped_big():
+ dt = pendulum.datetime(2013, 3, 31, 3, 0, 0, 0, tz="Europe/Paris")
+
+ assert_datetime(dt, 2013, 3, 31, 3, 0, 0, 0)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 7200
+ assert dt.is_dst()
+
+ dt = dt.subtract(days=1)
+
+ assert_datetime(dt, 2013, 3, 30, 3, 0, 0, 0)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 3600
+ assert not dt.is_dst()
+
+
+def test_subtract_time_to_new_transition_repeated():
+ dt = pendulum.datetime(2013, 10, 27, 2, 0, 0, 0, tz="Europe/Paris")
+
+ assert_datetime(dt, 2013, 10, 27, 2, 0, 0, 0)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 3600
+ assert not dt.is_dst()
+
+ dt = dt.subtract(microseconds=1)
+
+ assert_datetime(dt, 2013, 10, 27, 2, 59, 59, 999999)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 7200
+ assert dt.is_dst()
+
+ dt = pendulum.datetime(2013, 11, 3, 1, 0, 0, 0, tz="America/New_York")
+
+ assert_datetime(dt, 2013, 11, 3, 1, 0, 0, 0)
+ assert dt.timezone_name == "America/New_York"
+ assert dt.offset == -5 * 3600
+ assert not dt.is_dst()
+
+ dt = dt.subtract(microseconds=1)
+
+ assert_datetime(dt, 2013, 11, 3, 1, 59, 59, 999999)
+ assert dt.timezone_name == "America/New_York"
+ assert dt.offset == -4 * 3600
+ assert dt.is_dst()
+
+
+def test_subtract_time_to_new_transition_repeated_big():
+ dt = pendulum.datetime(2013, 10, 27, 2, 0, 0, 0, tz="Europe/Paris")
+
+ assert_datetime(dt, 2013, 10, 27, 2, 0, 0, 0)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 3600
+ assert not dt.is_dst()
+
+ dt = dt.subtract(days=1)
+
+ assert_datetime(dt, 2013, 10, 26, 2, 0, 0, 0)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 7200
+ assert dt.is_dst()
+
+
+def test_subtract_invalid_type():
+ d = pendulum.datetime(1975, 5, 21, 0, 0, 0)
+
+ with pytest.raises(TypeError):
+ d - "ab"
+
+ with pytest.raises(TypeError):
+ "ab" - d
+
+
+def test_subtract_negative_over_dls_transitioning_off():
+ just_before_dls_ends = pendulum.datetime(
+ 2019, 11, 3, 1, 30, tz="US/Pacific", fold=0
+ )
+ plus_10_hours = just_before_dls_ends + timedelta(hours=10)
+ minus_neg_10_hours = just_before_dls_ends - timedelta(hours=-10)
+
+ # 1:30-0700 becomes 10:30-0800
+ assert plus_10_hours.hour == 10
+ assert minus_neg_10_hours.hour == 10
+ assert just_before_dls_ends.is_dst()
+ assert not plus_10_hours.is_dst()
+ assert not minus_neg_10_hours.is_dst()
diff --git a/tests/datetime/test_timezone.py b/tests/datetime/test_timezone.py
new file mode 100644
index 0000000..343435d
--- /dev/null
+++ b/tests/datetime/test_timezone.py
@@ -0,0 +1,38 @@
+from __future__ import annotations
+
+import pendulum
+
+from tests.conftest import assert_datetime
+
+
+def test_in_timezone():
+ d = pendulum.datetime(2015, 1, 15, 18, 15, 34)
+ now = pendulum.datetime(2015, 1, 15, 18, 15, 34)
+ assert d.timezone_name == "UTC"
+ assert_datetime(d, now.year, now.month, now.day, now.hour, now.minute)
+
+ d = d.in_timezone("Europe/Paris")
+ assert d.timezone_name == "Europe/Paris"
+ assert_datetime(d, now.year, now.month, now.day, now.hour + 1, now.minute)
+
+
+def test_in_tz():
+ d = pendulum.datetime(2015, 1, 15, 18, 15, 34)
+ now = pendulum.datetime(2015, 1, 15, 18, 15, 34)
+ assert d.timezone_name == "UTC"
+ assert_datetime(d, now.year, now.month, now.day, now.hour, now.minute)
+
+ d = d.in_tz("Europe/Paris")
+ assert d.timezone_name == "Europe/Paris"
+ assert_datetime(d, now.year, now.month, now.day, now.hour + 1, now.minute)
+
+
+def test_astimezone():
+ d = pendulum.datetime(2015, 1, 15, 18, 15, 34)
+ now = pendulum.datetime(2015, 1, 15, 18, 15, 34)
+ assert d.timezone_name == "UTC"
+ assert_datetime(d, now.year, now.month, now.day, now.hour, now.minute)
+
+ d = d.astimezone(pendulum.timezone("Europe/Paris"))
+ assert d.timezone_name == "Europe/Paris"
+ assert_datetime(d, now.year, now.month, now.day, now.hour + 1, now.minute)
diff --git a/tests/duration/__init__.py b/tests/duration/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/duration/__init__.py
diff --git a/tests/duration/test_add_sub.py b/tests/duration/test_add_sub.py
new file mode 100644
index 0000000..193a493
--- /dev/null
+++ b/tests/duration/test_add_sub.py
@@ -0,0 +1,54 @@
+from __future__ import annotations
+
+from datetime import timedelta
+
+import pendulum
+
+from tests.conftest import assert_duration
+
+
+def test_add_interval():
+ p1 = pendulum.duration(days=23, seconds=32)
+ p2 = pendulum.duration(days=12, seconds=30)
+
+ p = p1 + p2
+ assert_duration(p, 0, 0, 5, 0, 0, 1, 2)
+
+
+def test_add_timedelta():
+ p1 = pendulum.duration(days=23, seconds=32)
+ p2 = timedelta(days=12, seconds=30)
+
+ p = p1 + p2
+ assert_duration(p, 0, 0, 5, 0, 0, 1, 2)
+
+
+def test_add_unsupported():
+ p = pendulum.duration(days=23, seconds=32)
+ assert NotImplemented == p.__add__(5)
+
+
+def test_sub_interval():
+ p1 = pendulum.duration(days=23, seconds=32)
+ p2 = pendulum.duration(days=12, seconds=28)
+
+ p = p1 - p2
+ assert_duration(p, 0, 0, 1, 4, 0, 0, 4)
+
+
+def test_sub_timedelta():
+ p1 = pendulum.duration(days=23, seconds=32)
+ p2 = timedelta(days=12, seconds=28)
+
+ p = p1 - p2
+ assert_duration(p, 0, 0, 1, 4, 0, 0, 4)
+
+
+def test_sub_unsupported():
+ p = pendulum.duration(days=23, seconds=32)
+ assert NotImplemented == p.__sub__(5)
+
+
+def test_neg():
+ p = pendulum.duration(days=23, seconds=32)
+ assert_duration(-p, 0, 0, -3, -2, 0, 0, -32)
diff --git a/tests/duration/test_arithmetic.py b/tests/duration/test_arithmetic.py
new file mode 100644
index 0000000..cba4d39
--- /dev/null
+++ b/tests/duration/test_arithmetic.py
@@ -0,0 +1,85 @@
+from __future__ import annotations
+
+import pendulum
+
+from tests.conftest import assert_duration
+
+
+def test_multiply():
+ it = pendulum.duration(days=6, seconds=34, microseconds=522222)
+ mul = it * 2
+
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 0, 0, 1, 5, 0, 1, 9, 44444)
+
+ it = pendulum.duration(days=6, seconds=34, microseconds=522222)
+ mul = 2 * it
+
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 0, 0, 1, 5, 0, 1, 9, 44444)
+
+ it = pendulum.duration(
+ years=2, months=3, weeks=4, days=6, seconds=34, microseconds=522222
+ )
+ mul = 2 * it
+
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 4, 6, 9, 5, 0, 1, 9, 44444)
+
+
+def test_divide():
+ it = pendulum.duration(days=2, seconds=34, microseconds=522222)
+ mul = it / 2
+
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 0, 0, 0, 1, 0, 0, 17, 261111)
+
+ it = pendulum.duration(days=2, seconds=35, microseconds=522222)
+ mul = it / 2
+
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 0, 0, 0, 1, 0, 0, 17, 761111)
+
+ it = pendulum.duration(days=2, seconds=35, microseconds=522222)
+ mul = it / 1.1
+
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 0, 0, 0, 1, 19, 38, 43, 202020)
+
+ it = pendulum.duration(years=2, months=4, days=2, seconds=35, microseconds=522222)
+ mul = it / 2
+
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 1, 2, 0, 1, 0, 0, 17, 761111)
+
+ it = pendulum.duration(years=2, months=4, days=2, seconds=35, microseconds=522222)
+ mul = it / 2.0
+
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 1, 2, 0, 1, 0, 0, 17, 761111)
+
+
+def test_floor_divide():
+ it = pendulum.duration(days=2, seconds=34, microseconds=522222)
+ mul = it // 2
+
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 0, 0, 0, 1, 0, 0, 17, 261111)
+
+ it = pendulum.duration(days=2, seconds=35, microseconds=522222)
+ mul = it // 3
+
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 0, 0, 0, 0, 16, 0, 11, 840740)
+
+ it = pendulum.duration(years=2, months=4, days=2, seconds=34, microseconds=522222)
+ mul = it // 2
+
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 1, 2, 0, 1, 0, 0, 17, 261111)
+
+ it = pendulum.duration(years=2, months=4, days=2, seconds=35, microseconds=522222)
+ mul = it // 3
+
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 0, 1, 0, 0, 16, 0, 11, 840740)
diff --git a/tests/duration/test_behavior.py b/tests/duration/test_behavior.py
new file mode 100644
index 0000000..a97bbde
--- /dev/null
+++ b/tests/duration/test_behavior.py
@@ -0,0 +1,21 @@
+from __future__ import annotations
+
+import pickle
+
+from datetime import timedelta
+
+import pendulum
+
+
+def test_pickle():
+ it = pendulum.duration(days=3, seconds=2456, microseconds=123456)
+ s = pickle.dumps(it)
+ it2 = pickle.loads(s)
+
+ assert it == it2
+
+
+def test_comparison_to_timedelta():
+ duration = pendulum.duration(days=3)
+
+ assert duration < timedelta(days=4)
diff --git a/tests/duration/test_construct.py b/tests/duration/test_construct.py
new file mode 100644
index 0000000..aaa5390
--- /dev/null
+++ b/tests/duration/test_construct.py
@@ -0,0 +1,99 @@
+from __future__ import annotations
+
+from datetime import timedelta
+
+import pytest
+
+import pendulum
+
+from pendulum.duration import AbsoluteDuration
+from tests.conftest import assert_duration
+
+
+def test_defaults():
+ pi = pendulum.duration()
+ assert_duration(pi, 0, 0, 0, 0, 0, 0, 0)
+
+
+def test_years():
+ pi = pendulum.duration(years=2)
+ assert_duration(pi, years=2, weeks=0)
+ assert pi.days == 730
+ assert pi.total_seconds() == 63072000
+
+
+def test_months():
+ pi = pendulum.duration(months=3)
+ assert_duration(pi, months=3, weeks=0)
+ assert pi.days == 90
+ assert pi.total_seconds() == 7776000
+
+
+def test_weeks():
+ pi = pendulum.duration(days=365)
+ assert_duration(pi, weeks=52)
+
+ pi = pendulum.duration(days=13)
+ assert_duration(pi, weeks=1)
+
+
+def test_days():
+ pi = pendulum.duration(days=6)
+ assert_duration(pi, 0, 0, 0, 6, 0, 0, 0)
+
+ pi = pendulum.duration(days=16)
+ assert_duration(pi, 0, 0, 2, 2, 0, 0, 0)
+
+
+def test_hours():
+ pi = pendulum.duration(seconds=3600 * 3)
+ assert_duration(pi, 0, 0, 0, 0, 3, 0, 0)
+
+
+def test_minutes():
+ pi = pendulum.duration(seconds=60 * 3)
+ assert_duration(pi, 0, 0, 0, 0, 0, 3, 0)
+
+ pi = pendulum.duration(seconds=60 * 3 + 12)
+ assert_duration(pi, 0, 0, 0, 0, 0, 3, 12)
+
+
+def test_all():
+ pi = pendulum.duration(
+ years=2, months=3, days=1177, seconds=7284, microseconds=1000000
+ )
+ assert_duration(pi, 2, 3, 168, 1, 2, 1, 25)
+ assert pi.days == 1997
+ assert pi.seconds == 7285
+
+
+def test_absolute_interval():
+ pi = AbsoluteDuration(days=-1177, seconds=-7284, microseconds=-1000001)
+ assert_duration(pi, 0, 0, 168, 1, 2, 1, 25)
+ assert pi.microseconds == 1
+ assert pi.invert
+
+
+def test_invert():
+ pi = pendulum.duration(days=1177, seconds=7284, microseconds=1000000)
+ assert not pi.invert
+
+ pi = pendulum.duration(days=-1177, seconds=-7284, microseconds=-1000000)
+ assert pi.invert
+
+
+def test_as_timedelta():
+ pi = pendulum.duration(seconds=3456.123456)
+ assert_duration(pi, 0, 0, 0, 0, 0, 57, 36, 123456)
+ delta = pi.as_timedelta()
+ assert isinstance(delta, timedelta)
+ assert delta.total_seconds() == 3456.123456
+ assert delta.seconds == 3456
+
+
+def test_float_years_and_months():
+ with pytest.raises(ValueError):
+ pendulum.duration(years=1.5)
+
+ with pytest.raises(ValueError):
+ pendulum.duration(months=1.5)
diff --git a/tests/duration/test_in_methods.py b/tests/duration/test_in_methods.py
new file mode 100644
index 0000000..4527257
--- /dev/null
+++ b/tests/duration/test_in_methods.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+import pendulum
+
+
+def test_in_weeks():
+ it = pendulum.duration(days=17)
+ assert it.in_weeks() == 2
+
+
+def test_in_days():
+ it = pendulum.duration(days=3)
+ assert it.in_days() == 3
+
+
+def test_in_hours():
+ it = pendulum.duration(days=3, minutes=72)
+ assert it.in_hours() == 73
+
+
+def test_in_minutes():
+ it = pendulum.duration(minutes=6, seconds=72)
+ assert it.in_minutes() == 7
+
+
+def test_in_seconds():
+ it = pendulum.duration(seconds=72)
+ assert it.in_seconds() == 72
diff --git a/tests/duration/test_in_words.py b/tests/duration/test_in_words.py
new file mode 100644
index 0000000..c0a1a1f
--- /dev/null
+++ b/tests/duration/test_in_words.py
@@ -0,0 +1,77 @@
+from __future__ import annotations
+
+import pendulum
+
+
+def test_week():
+ assert pendulum.duration(days=364).in_words() == "52 weeks"
+ assert pendulum.duration(days=7).in_words() == "1 week"
+
+
+def test_week_to_string():
+ assert str(pendulum.duration(days=364)) == "52 weeks"
+ assert str(pendulum.duration(days=7)) == "1 week"
+
+
+def test_weeks_and_day():
+ assert pendulum.duration(days=365).in_words() == "52 weeks 1 day"
+
+
+def test_all():
+ pi = pendulum.duration(
+ years=2, months=3, days=1177, seconds=7284, microseconds=1000000
+ )
+
+ expected = "2 years 3 months 168 weeks 1 day 2 hours 1 minute 25 seconds"
+ assert pi.in_words() == expected
+
+
+def test_in_french():
+ pi = pendulum.duration(
+ years=2, months=3, days=1177, seconds=7284, microseconds=1000000
+ )
+
+ expected = "2 ans 3 mois 168 semaines 1 jour 2 heures 1 minute 25 secondes"
+ assert pi.in_words(locale="fr") == expected
+
+
+def test_repr():
+ pi = pendulum.duration(
+ years=2, months=3, days=1177, seconds=7284, microseconds=1000000
+ )
+
+ expected = (
+ "Duration(years=2, months=3, weeks=168, days=1, hours=2, minutes=1, seconds=25)"
+ )
+ assert repr(pi) == expected
+
+
+def test_singular_negative_values():
+ pi = pendulum.duration(days=-1)
+
+ assert pi.in_words() == "-1 day"
+
+
+def test_separator():
+ pi = pendulum.duration(days=1177, seconds=7284, microseconds=1000000)
+
+ expected = "168 weeks, 1 day, 2 hours, 1 minute, 25 seconds"
+ assert pi.in_words(separator=", ") == expected
+
+
+def test_subseconds():
+ pi = pendulum.duration(microseconds=123456)
+
+ assert pi.in_words() == "0.12 second"
+
+
+def test_subseconds_with_seconds():
+ pi = pendulum.duration(seconds=12, microseconds=123456)
+
+ assert pi.in_words() == "12 seconds"
+
+
+def test_duration_with_all_zero_values():
+ pi = pendulum.duration()
+
+ assert pi.in_words() == "0 microseconds"
diff --git a/tests/duration/test_total_methods.py b/tests/duration/test_total_methods.py
new file mode 100644
index 0000000..aabcd6c
--- /dev/null
+++ b/tests/duration/test_total_methods.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+import pendulum
+
+
+def test_in_weeks():
+ it = pendulum.duration(days=17)
+ assert round(it.total_weeks(), 2) == 2.43
+
+
+def test_in_days():
+ it = pendulum.duration(days=3)
+ assert it.total_days() == 3
+
+
+def test_in_hours():
+ it = pendulum.duration(days=3, minutes=72)
+ assert it.total_hours() == 73.2
+
+
+def test_in_minutes():
+ it = pendulum.duration(minutes=6, seconds=72)
+ assert it.total_minutes() == 7.2
+
+
+def test_in_seconds():
+ it = pendulum.duration(seconds=72, microseconds=123456)
+ assert it.total_seconds() == 72.123456
diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/fixtures/__init__.py
diff --git a/tests/fixtures/tz/Paris b/tests/fixtures/tz/Paris
new file mode 100644
index 0000000..cf6e2e2
--- /dev/null
+++ b/tests/fixtures/tz/Paris
Binary files differ
diff --git a/tests/fixtures/tz/clock/etc/sysconfig/clock b/tests/fixtures/tz/clock/etc/sysconfig/clock
new file mode 100644
index 0000000..285e68b
--- /dev/null
+++ b/tests/fixtures/tz/clock/etc/sysconfig/clock
@@ -0,0 +1 @@
+ZONE="/usr/share/zoneinfo/Europe/Zurich"
diff --git a/tests/fixtures/tz/symlink/etc/localtime b/tests/fixtures/tz/symlink/etc/localtime
new file mode 120000
index 0000000..d9726ef
--- /dev/null
+++ b/tests/fixtures/tz/symlink/etc/localtime
@@ -0,0 +1 @@
+../usr/share/zoneinfo/Europe/Paris \ No newline at end of file
diff --git a/tests/fixtures/tz/symlink/usr/share/zoneinfo/Europe/Paris b/tests/fixtures/tz/symlink/usr/share/zoneinfo/Europe/Paris
new file mode 100644
index 0000000..cf6e2e2
--- /dev/null
+++ b/tests/fixtures/tz/symlink/usr/share/zoneinfo/Europe/Paris
Binary files differ
diff --git a/tests/fixtures/tz/timezone_dir/etc/localtime b/tests/fixtures/tz/timezone_dir/etc/localtime
new file mode 120000
index 0000000..d9726ef
--- /dev/null
+++ b/tests/fixtures/tz/timezone_dir/etc/localtime
@@ -0,0 +1 @@
+../usr/share/zoneinfo/Europe/Paris \ No newline at end of file
diff --git a/tests/fixtures/tz/timezone_dir/etc/timezone/blank.md b/tests/fixtures/tz/timezone_dir/etc/timezone/blank.md
new file mode 100644
index 0000000..a3a9320
--- /dev/null
+++ b/tests/fixtures/tz/timezone_dir/etc/timezone/blank.md
@@ -0,0 +1,5 @@
+# Blank file
+
+Necessary for environments, which not handle empty folder synchronization well, to be sure the parent folder is created.
+
+The `/etc/timezone` folder is necessary to ensure that the package not fail if that folder exists.
diff --git a/tests/fixtures/tz/timezone_dir/usr/share/zoneinfo/Europe/Paris b/tests/fixtures/tz/timezone_dir/usr/share/zoneinfo/Europe/Paris
new file mode 100644
index 0000000..cf6e2e2
--- /dev/null
+++ b/tests/fixtures/tz/timezone_dir/usr/share/zoneinfo/Europe/Paris
Binary files differ
diff --git a/tests/formatting/__init__.py b/tests/formatting/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/formatting/__init__.py
diff --git a/tests/formatting/test_formatter.py b/tests/formatting/test_formatter.py
new file mode 100644
index 0000000..76c7a7f
--- /dev/null
+++ b/tests/formatting/test_formatter.py
@@ -0,0 +1,263 @@
+from __future__ import annotations
+
+import pytest
+
+import pendulum
+
+from pendulum.formatting import Formatter
+from pendulum.locales.locale import Locale
+
+
+@pytest.fixture(autouse=True)
+def setup():
+ Locale._cache["dummy"] = {}
+
+ yield
+
+ del Locale._cache["dummy"]
+
+
+def test_year_tokens():
+ d = pendulum.datetime(2009, 1, 14, 15, 25, 50, 123456)
+ f = Formatter()
+
+ assert f.format(d, "YYYY") == "2009"
+ assert f.format(d, "YY") == "09"
+ assert f.format(d, "Y") == "2009"
+
+
+def test_quarter_tokens():
+ f = Formatter()
+ d = pendulum.datetime(1985, 1, 4)
+ assert f.format(d, "Q") == "1"
+
+ d = pendulum.datetime(2029, 8, 1)
+ assert f.format(d, "Q") == "3"
+
+ d = pendulum.datetime(1985, 1, 4)
+ assert f.format(d, "Qo") == "1st"
+
+ d = pendulum.datetime(2029, 8, 1)
+ assert f.format(d, "Qo") == "3rd"
+
+ d = pendulum.datetime(1985, 1, 4)
+ assert f.format(d, "Qo", locale="fr") == "1er"
+
+ d = pendulum.datetime(2029, 8, 1)
+ assert f.format(d, "Qo", locale="fr") == "3e"
+
+
+def test_month_tokens():
+ f = Formatter()
+ d = pendulum.datetime(2016, 3, 24)
+ assert f.format(d, "MM") == "03"
+ assert f.format(d, "M") == "3"
+
+ assert f.format(d, "MMM") == "Mar"
+ assert f.format(d, "MMMM") == "March"
+ assert f.format(d, "Mo") == "3rd"
+
+ assert f.format(d, "MMM", locale="fr") == "mars"
+ assert f.format(d, "MMMM", locale="fr") == "mars"
+ assert f.format(d, "Mo", locale="fr") == "3e"
+
+
+def test_day_tokens():
+ f = Formatter()
+ d = pendulum.datetime(2016, 3, 7)
+ assert f.format(d, "DD") == "07"
+ assert f.format(d, "D") == "7"
+
+ assert f.format(d, "Do") == "7th"
+ assert f.format(d.first_of("month"), "Do") == "1st"
+
+ assert f.format(d, "Do", locale="fr") == "7e"
+ assert f.format(d.first_of("month"), "Do", locale="fr") == "1er"
+
+
+def test_day_of_year():
+ f = Formatter()
+ d = pendulum.datetime(2016, 8, 28)
+ assert f.format(d, "DDDD") == "241"
+ assert f.format(d, "DDD") == "241"
+ assert f.format(d.start_of("year"), "DDDD") == "001"
+ assert f.format(d.start_of("year"), "DDD") == "1"
+
+ assert f.format(d, "DDDo") == "241st"
+ assert f.format(d.add(days=3), "DDDo") == "244th"
+
+ assert f.format(d, "DDDo", locale="fr") == "241e"
+ assert f.format(d.add(days=3), "DDDo", locale="fr") == "244e"
+
+
+def test_week_of_year():
+ f = Formatter()
+ d = pendulum.datetime(2016, 8, 28)
+
+ assert f.format(d, "wo") == "34th"
+
+
+def test_day_of_week():
+ f = Formatter()
+ d = pendulum.datetime(2016, 8, 28)
+ assert f.format(d, "d") == "0"
+
+ assert f.format(d, "dd") == "Su"
+ assert f.format(d, "ddd") == "Sun"
+ assert f.format(d, "dddd") == "Sunday"
+
+ assert f.format(d, "dd", locale="fr") == "di"
+ assert f.format(d, "ddd", locale="fr") == "dim."
+ assert f.format(d, "dddd", locale="fr") == "dimanche"
+
+ assert f.format(d, "do") == "0th"
+
+
+def test_day_of_iso_week():
+ f = Formatter()
+ d = pendulum.datetime(2016, 8, 28)
+ assert f.format(d, "E") == "7"
+
+
+def test_am_pm():
+ f = Formatter()
+ d = pendulum.datetime(2016, 8, 28, 23)
+ assert f.format(d, "A") == "PM"
+ assert f.format(d.set(hour=11), "A") == "AM"
+
+
+def test_hour():
+ f = Formatter()
+ d = pendulum.datetime(2016, 8, 28, 7)
+ assert f.format(d, "H") == "7"
+ assert f.format(d, "HH") == "07"
+
+ d = pendulum.datetime(2016, 8, 28, 0)
+ assert f.format(d, "h") == "12"
+ assert f.format(d, "hh") == "12"
+
+
+def test_minute():
+ f = Formatter()
+ d = pendulum.datetime(2016, 8, 28, 7, 3)
+ assert f.format(d, "m") == "3"
+ assert f.format(d, "mm") == "03"
+
+
+def test_second():
+ f = Formatter()
+ d = pendulum.datetime(2016, 8, 28, 7, 3, 6)
+ assert f.format(d, "s") == "6"
+ assert f.format(d, "ss") == "06"
+
+
+def test_fractional_second():
+ f = Formatter()
+ d = pendulum.datetime(2016, 8, 28, 7, 3, 6, 123456)
+ assert f.format(d, "S") == "1"
+ assert f.format(d, "SS") == "12"
+ assert f.format(d, "SSS") == "123"
+ assert f.format(d, "SSSS") == "1234"
+ assert f.format(d, "SSSSS") == "12345"
+ assert f.format(d, "SSSSSS") == "123456"
+
+ d = pendulum.datetime(2016, 8, 28, 7, 3, 6, 0)
+ assert f.format(d, "S") == "0"
+ assert f.format(d, "SS") == "00"
+ assert f.format(d, "SSS") == "000"
+ assert f.format(d, "SSSS") == "0000"
+ assert f.format(d, "SSSSS") == "00000"
+ assert f.format(d, "SSSSSS") == "000000"
+
+ d = pendulum.datetime(2016, 8, 28, 7, 3, 6, 123)
+ assert f.format(d, "S") == "0"
+ assert f.format(d, "SS") == "00"
+ assert f.format(d, "SSS") == "000"
+ assert f.format(d, "SSSS") == "0001"
+ assert f.format(d, "SSSSS") == "00012"
+ assert f.format(d, "SSSSSS") == "000123"
+
+
+def test_timezone():
+ f = Formatter()
+ d = pendulum.datetime(2016, 8, 28, 7, 3, 6, 123456, tz="Europe/Paris")
+ assert f.format(d, "zz") == "CEST"
+ assert f.format(d, "z") == "Europe/Paris"
+
+ d = pendulum.datetime(2016, 1, 28, 7, 3, 6, 123456, tz="Europe/Paris")
+ assert f.format(d, "zz") == "CET"
+ assert f.format(d, "z") == "Europe/Paris"
+
+
+def test_timezone_offset():
+ f = Formatter()
+ d = pendulum.datetime(2016, 8, 28, 7, 3, 6, 123456, tz="Europe/Paris")
+ assert f.format(d, "ZZ") == "+0200"
+ assert f.format(d, "Z") == "+02:00"
+
+ d = pendulum.datetime(2016, 1, 28, 7, 3, 6, 123456, tz="Europe/Paris")
+ assert f.format(d, "ZZ") == "+0100"
+ assert f.format(d, "Z") == "+01:00"
+
+ d = pendulum.datetime(2016, 1, 28, 7, 3, 6, 123456, tz="America/Guayaquil")
+ assert f.format(d, "ZZ") == "-0500"
+ assert f.format(d, "Z") == "-05:00"
+
+
+def test_timestamp():
+ f = Formatter()
+ d = pendulum.datetime(1970, 1, 1)
+ assert f.format(d, "X") == "0"
+ assert f.format(d.add(days=1), "X") == "86400"
+
+
+def test_timestamp_milliseconds():
+ f = Formatter()
+ d = pendulum.datetime(1970, 1, 1)
+ assert f.format(d, "x") == "0"
+ assert f.format(d.add(days=1), "x") == "86400000"
+ assert f.format(d.add(days=1, microseconds=129123), "x") == "86400129"
+
+
+def test_date_formats():
+ f = Formatter()
+ d = pendulum.datetime(2016, 8, 28, 7, 3, 6, 123456)
+ assert f.format(d, "LT") == "7:03 AM"
+ assert f.format(d, "LTS") == "7:03:06 AM"
+ assert f.format(d, "L") == "08/28/2016"
+ assert f.format(d, "LL") == "August 28, 2016"
+ assert f.format(d, "LLL") == "August 28, 2016 7:03 AM"
+ assert f.format(d, "LLLL") == "Sunday, August 28, 2016 7:03 AM"
+
+ assert f.format(d, "LT", locale="fr") == "07:03"
+ assert f.format(d, "LTS", locale="fr") == "07:03:06"
+ assert f.format(d, "L", locale="fr") == "28/08/2016"
+ assert f.format(d, "LL", locale="fr") == "28 août 2016"
+ assert f.format(d, "LLL", locale="fr") == "28 août 2016 07:03"
+ assert f.format(d, "LLLL", locale="fr") == "dimanche 28 août 2016 07:03"
+
+
+def test_escape():
+ f = Formatter()
+ d = pendulum.datetime(2016, 8, 28)
+ assert f.format(d, r"[YYYY] YYYY \[YYYY\]") == "YYYY 2016 [2016]"
+ assert f.format(d, r"\D D \\D") == "D 28 \\28"
+
+
+def test_date_formats_missing():
+ f = Formatter()
+ d = pendulum.datetime(2016, 8, 28, 7, 3, 6, 123456)
+
+ assert f.format(d, "LT", locale="dummy") == "7:03 AM"
+ assert f.format(d, "LTS", locale="dummy") == "7:03:06 AM"
+ assert f.format(d, "L", locale="dummy") == "08/28/2016"
+ assert f.format(d, "LL", locale="dummy") == "August 28, 2016"
+ assert f.format(d, "LLL", locale="dummy") == "August 28, 2016 7:03 AM"
+ assert f.format(d, "LLLL", locale="dummy") == "Sunday, August 28, 2016 7:03 AM"
+
+
+def test_unknown_token():
+ f = Formatter()
+ d = pendulum.datetime(2016, 8, 28, 7, 3, 6, 123456)
+
+ assert f.format(d, "J") == "J"
diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/helpers/__init__.py
diff --git a/tests/helpers/test_local_time.py b/tests/helpers/test_local_time.py
new file mode 100644
index 0000000..0563107
--- /dev/null
+++ b/tests/helpers/test_local_time.py
@@ -0,0 +1,31 @@
+from __future__ import annotations
+
+import pendulum
+
+from pendulum.helpers import local_time
+
+
+def test_local_time_positive_integer():
+ d = pendulum.datetime(2016, 8, 7, 12, 34, 56, 123456)
+
+ t = local_time(d.int_timestamp, 0, d.microsecond)
+ assert d.year == t[0]
+ assert d.month == t[1]
+ assert d.day == t[2]
+ assert d.hour == t[3]
+ assert d.minute == t[4]
+ assert d.second == t[5]
+ assert d.microsecond == t[6]
+
+
+def test_local_time_negative_integer():
+ d = pendulum.datetime(1951, 8, 7, 12, 34, 56, 123456)
+
+ t = local_time(d.int_timestamp, 0, d.microsecond)
+ assert d.year == t[0]
+ assert d.month == t[1]
+ assert d.day == t[2]
+ assert d.hour == t[3]
+ assert d.minute == t[4]
+ assert d.second == t[5]
+ assert d.microsecond == t[6]
diff --git a/tests/interval/__init__.py b/tests/interval/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/interval/__init__.py
diff --git a/tests/interval/test_add_subtract.py b/tests/interval/test_add_subtract.py
new file mode 100644
index 0000000..88525a3
--- /dev/null
+++ b/tests/interval/test_add_subtract.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+import pendulum
+
+
+def test_dst_add():
+ start = pendulum.datetime(2017, 3, 7, tz="America/Toronto")
+ end = start.add(days=6)
+ period = end - start
+ new_end = start + period
+
+ assert new_end == end
+
+
+def test_dst_add_non_variable_units():
+ start = pendulum.datetime(2013, 3, 31, 1, 30, tz="Europe/Paris")
+ end = start.add(hours=1)
+ period = end - start
+ new_end = start + period
+
+ assert new_end == end
+
+
+def test_dst_subtract():
+ start = pendulum.datetime(2017, 3, 7, tz="America/Toronto")
+ end = start.add(days=6)
+ period = end - start
+ new_start = end - period
+
+ assert new_start == start
+
+
+def test_naive_subtract():
+ start = pendulum.naive(2013, 3, 31, 1, 30)
+ end = start.add(hours=1)
+ period = end - start
+ new_end = start + period
+
+ assert new_end == end
+
+
+def test_negative_difference_subtract():
+ start = pendulum.datetime(2018, 5, 28, 12, 34, 56, 123456)
+ end = pendulum.datetime(2018, 1, 1)
+
+ period = end - start
+ new_end = start + period
+
+ assert new_end == end
diff --git a/tests/interval/test_arithmetic.py b/tests/interval/test_arithmetic.py
new file mode 100644
index 0000000..e5ba01f
--- /dev/null
+++ b/tests/interval/test_arithmetic.py
@@ -0,0 +1,53 @@
+from __future__ import annotations
+
+import pendulum
+
+from tests.conftest import assert_duration
+
+
+def test_multiply():
+ dt1 = pendulum.DateTime(2016, 8, 7, 12, 34, 56)
+ dt2 = dt1.add(days=6, seconds=34)
+ it = pendulum.interval(dt1, dt2)
+ mul = it * 2
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 0, 0, 1, 5, 0, 1, 8)
+
+ dt1 = pendulum.DateTime(2016, 8, 7, 12, 34, 56)
+ dt2 = dt1.add(days=6, seconds=34)
+ it = pendulum.interval(dt1, dt2)
+ mul = it * 2
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 0, 0, 1, 5, 0, 1, 8)
+
+
+def test_divide():
+ dt1 = pendulum.DateTime(2016, 8, 7, 12, 34, 56)
+ dt2 = dt1.add(days=2, seconds=34)
+ it = pendulum.interval(dt1, dt2)
+ mul = it / 2
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 0, 0, 0, 1, 0, 0, 17)
+
+ dt1 = pendulum.DateTime(2016, 8, 7, 12, 34, 56)
+ dt2 = dt1.add(days=2, seconds=35)
+ it = pendulum.interval(dt1, dt2)
+ mul = it / 2
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 0, 0, 0, 1, 0, 0, 17)
+
+
+def test_floor_divide():
+ dt1 = pendulum.DateTime(2016, 8, 7, 12, 34, 56)
+ dt2 = dt1.add(days=2, seconds=34)
+ it = pendulum.interval(dt1, dt2)
+ mul = it // 2
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 0, 0, 0, 1, 0, 0, 17)
+
+ dt1 = pendulum.DateTime(2016, 8, 7, 12, 34, 56)
+ dt2 = dt1.add(days=2, seconds=35)
+ it = pendulum.interval(dt1, dt2)
+ mul = it // 3
+ assert isinstance(mul, pendulum.Duration)
+ assert_duration(mul, 0, 0, 0, 0, 16, 0, 11)
diff --git a/tests/interval/test_behavior.py b/tests/interval/test_behavior.py
new file mode 100644
index 0000000..b5e057a
--- /dev/null
+++ b/tests/interval/test_behavior.py
@@ -0,0 +1,54 @@
+from __future__ import annotations
+
+import pickle
+
+from datetime import timedelta
+
+import pendulum
+
+
+def test_pickle():
+ dt1 = pendulum.datetime(2016, 11, 18)
+ dt2 = pendulum.datetime(2016, 11, 20)
+
+ p = pendulum.interval(dt1, dt2)
+ s = pickle.dumps(p)
+ p2 = pickle.loads(s)
+
+ assert p.start == p2.start
+ assert p.end == p2.end
+ assert p.invert == p2.invert
+
+ p = pendulum.interval(dt2, dt1)
+ s = pickle.dumps(p)
+ p2 = pickle.loads(s)
+
+ assert p.start == p2.start
+ assert p.end == p2.end
+ assert p.invert == p2.invert
+
+ p = pendulum.interval(dt2, dt1, True)
+ s = pickle.dumps(p)
+ p2 = pickle.loads(s)
+
+ assert p.start == p2.start
+ assert p.end == p2.end
+ assert p.invert == p2.invert
+
+
+def test_comparison_to_timedelta():
+ dt1 = pendulum.datetime(2016, 11, 18)
+ dt2 = pendulum.datetime(2016, 11, 20)
+
+ period = dt2 - dt1
+
+ assert period < timedelta(days=4)
+
+
+def test_equality_to_timedelta():
+ dt1 = pendulum.datetime(2016, 11, 18)
+ dt2 = pendulum.datetime(2016, 11, 20)
+
+ period = dt2 - dt1
+
+ assert period == timedelta(days=2)
diff --git a/tests/interval/test_construct.py b/tests/interval/test_construct.py
new file mode 100644
index 0000000..024e741
--- /dev/null
+++ b/tests/interval/test_construct.py
@@ -0,0 +1,121 @@
+from __future__ import annotations
+
+from datetime import datetime
+
+import pendulum
+
+from tests.conftest import assert_datetime
+
+
+def test_with_datetimes():
+ dt1 = datetime(2000, 1, 1)
+ dt2 = datetime(2000, 1, 31)
+ p = pendulum.interval(dt1, dt2)
+
+ assert isinstance(p.start, pendulum.DateTime)
+ assert isinstance(p.end, pendulum.DateTime)
+ assert_datetime(p.start, 2000, 1, 1)
+ assert_datetime(p.end, 2000, 1, 31)
+
+
+def test_with_pendulum():
+ dt1 = pendulum.DateTime(2000, 1, 1)
+ dt2 = pendulum.DateTime(2000, 1, 31)
+ p = pendulum.interval(dt1, dt2)
+
+ assert_datetime(p.start, 2000, 1, 1)
+ assert_datetime(p.end, 2000, 1, 31)
+
+
+def test_inverted():
+ dt1 = pendulum.DateTime(2000, 1, 1)
+ dt2 = pendulum.DateTime(2000, 1, 31)
+ p = pendulum.interval(dt2, dt1)
+
+ assert_datetime(p.start, 2000, 1, 31)
+ assert_datetime(p.end, 2000, 1, 1)
+
+
+def test_inverted_and_absolute():
+ dt1 = pendulum.DateTime(2000, 1, 1)
+ dt2 = pendulum.DateTime(2000, 1, 31)
+ p = pendulum.interval(dt2, dt1, True)
+
+ assert_datetime(p.start, 2000, 1, 1)
+ assert_datetime(p.end, 2000, 1, 31)
+
+
+def test_accuracy():
+ dt1 = pendulum.DateTime(2000, 11, 20)
+ dt2 = pendulum.DateTime(2000, 11, 25)
+ dt3 = pendulum.DateTime(2016, 11, 5)
+ p1 = pendulum.interval(dt1, dt3)
+ p2 = pendulum.interval(dt2, dt3)
+
+ assert p1.years == 15
+ assert p1.in_years() == 15
+ assert p1.months == 11
+ assert p1.in_months() == 191
+ assert p1.days == 5829
+ assert p1.remaining_days == 2
+ assert p1.in_days() == 5829
+
+ assert p2.years == 15
+ assert p2.in_years() == 15
+ assert p2.months == 11
+ assert p2.in_months() == 191
+ assert p2.days == 5824
+ assert p2.remaining_days == 4
+ assert p2.in_days() == 5824
+
+
+def test_dst_transition():
+ start = pendulum.datetime(2017, 3, 7, tz="America/Toronto")
+ end = start.add(days=6)
+ period = end - start
+
+ assert period.days == 5
+ assert period.seconds == 82800
+
+ assert period.remaining_days == 6
+ assert period.hours == 0
+ assert period.remaining_seconds == 0
+
+ assert period.in_days() == 6
+ assert period.in_hours() == 5 * 24 + 23
+
+
+def test_timedelta_behavior():
+ dt1 = pendulum.DateTime(2000, 11, 20, 1)
+ dt2 = pendulum.DateTime(2000, 11, 25, 2)
+ dt3 = pendulum.DateTime(2016, 11, 5, 3)
+
+ p1 = pendulum.interval(dt1, dt3)
+ p2 = pendulum.interval(dt2, dt3)
+ it1 = p1.as_timedelta()
+ it2 = p2.as_timedelta()
+
+ assert it1.total_seconds() == p1.total_seconds()
+ assert it2.total_seconds() == p2.total_seconds()
+ assert it1.days == p1.days
+ assert it2.days == p2.days
+ assert it1.seconds == p1.seconds
+ assert it2.seconds == p2.seconds
+ assert it1.microseconds == p1.microseconds
+ assert it2.microseconds == p2.microseconds
+
+
+def test_different_timezones_same_time():
+ dt1 = pendulum.datetime(2013, 3, 31, 1, 30, tz="Europe/Paris")
+ dt2 = pendulum.datetime(2013, 4, 1, 1, 30, tz="Europe/Paris")
+ period = dt2 - dt1
+
+ assert period.in_words() == "1 day"
+ assert period.in_hours() == 23
+
+ dt1 = pendulum.datetime(2013, 3, 31, 1, 30, tz="Europe/Paris")
+ dt2 = pendulum.datetime(2013, 4, 1, 1, 30, tz="America/Toronto")
+ period = dt2 - dt1
+
+ assert period.in_words() == "1 day 5 hours"
+ assert period.in_hours() == 29
diff --git a/tests/interval/test_hashing.py b/tests/interval/test_hashing.py
new file mode 100644
index 0000000..c18502f
--- /dev/null
+++ b/tests/interval/test_hashing.py
@@ -0,0 +1,23 @@
+from __future__ import annotations
+
+import pendulum
+
+
+def test_periods_with_same_duration_and_different_dates():
+ day1 = pendulum.DateTime(2018, 1, 1)
+ day2 = pendulum.DateTime(2018, 1, 2)
+ day3 = pendulum.DateTime(2018, 1, 2)
+
+ period1 = day2 - day1
+ period2 = day3 - day2
+
+ assert period1 != period2
+ assert len({period1, period2}) == 2
+
+
+def test_periods_with_same_dates():
+ period1 = pendulum.DateTime(2018, 1, 2) - pendulum.DateTime(2018, 1, 1)
+ period2 = pendulum.DateTime(2018, 1, 2) - pendulum.DateTime(2018, 1, 1)
+
+ assert period1 == period2
+ assert len({period1, period2}) == 1
diff --git a/tests/interval/test_in_words.py b/tests/interval/test_in_words.py
new file mode 100644
index 0000000..410e11f
--- /dev/null
+++ b/tests/interval/test_in_words.py
@@ -0,0 +1,70 @@
+from __future__ import annotations
+
+import pendulum
+
+
+def test_week():
+ start_date = pendulum.datetime(2012, 1, 1)
+ period = pendulum.interval(start=start_date, end=start_date.add(weeks=1))
+ assert period.in_words() == "1 week"
+
+
+def test_week_and_day():
+ start_date = pendulum.datetime(2012, 1, 1)
+ period = pendulum.interval(start=start_date, end=start_date.add(weeks=1, days=1))
+ assert period.in_words() == "1 week 1 day"
+
+
+def test_all():
+ start_date = pendulum.datetime(2012, 1, 1)
+ period = pendulum.interval(
+ start=start_date,
+ end=start_date.add(years=1, months=1, days=1, seconds=1, microseconds=1),
+ )
+ assert period.in_words() == "1 year 1 month 1 day 1 second"
+
+
+def test_in_french():
+ start_date = pendulum.datetime(2012, 1, 1)
+ period = pendulum.interval(
+ start=start_date,
+ end=start_date.add(years=1, months=1, days=1, seconds=1, microseconds=1),
+ )
+ assert period.in_words(locale="fr") == "1 an 1 mois 1 jour 1 seconde"
+
+
+def test_singular_negative_values():
+ start_date = pendulum.datetime(2012, 1, 1)
+ period = pendulum.interval(start=start_date, end=start_date.subtract(days=1))
+ assert period.in_words() == "-1 day"
+
+
+def test_separator():
+ start_date = pendulum.datetime(2012, 1, 1)
+ period = pendulum.interval(
+ start=start_date,
+ end=start_date.add(years=1, months=1, days=1, seconds=1, microseconds=1),
+ )
+ assert period.in_words(separator=", ") == "1 year, 1 month, 1 day, 1 second"
+
+
+def test_subseconds():
+ start_date = pendulum.datetime(2012, 1, 1)
+ period = pendulum.interval(
+ start=start_date, end=start_date.add(microseconds=123456)
+ )
+ assert period.in_words() == "0.12 second"
+
+
+def test_subseconds_with_seconds():
+ start_date = pendulum.datetime(2012, 1, 1)
+ period = pendulum.interval(
+ start=start_date, end=start_date.add(seconds=12, microseconds=123456)
+ )
+ assert period.in_words() == "12 seconds"
+
+
+def test_zero_period():
+ start_date = pendulum.datetime(2012, 1, 1)
+ period = pendulum.interval(start=start_date, end=start_date)
+ assert period.in_words() == "0 microseconds"
diff --git a/tests/interval/test_range.py b/tests/interval/test_range.py
new file mode 100644
index 0000000..28fe1ff
--- /dev/null
+++ b/tests/interval/test_range.py
@@ -0,0 +1,119 @@
+from __future__ import annotations
+
+import pendulum
+
+from pendulum.interval import Interval
+from tests.conftest import assert_datetime
+
+
+def test_range():
+ dt1 = pendulum.datetime(2000, 1, 1, 12, 45, 37)
+ dt2 = pendulum.datetime(2000, 1, 31, 12, 45, 37)
+
+ p = Interval(dt1, dt2)
+ r = list(p.range("days"))
+
+ assert len(r) == 31
+ assert_datetime(r[0], 2000, 1, 1, 12, 45, 37)
+ assert_datetime(r[-1], 2000, 1, 31, 12, 45, 37)
+
+
+def test_range_no_overflow():
+ dt1 = pendulum.datetime(2000, 1, 1, 12, 45, 37)
+ dt2 = pendulum.datetime(2000, 1, 31, 11, 45, 37)
+
+ p = Interval(dt1, dt2)
+ r = list(p.range("days"))
+
+ assert len(r) == 30
+ assert_datetime(r[0], 2000, 1, 1, 12, 45, 37)
+ assert_datetime(r[-1], 2000, 1, 30, 12, 45, 37)
+
+
+def test_range_inverted():
+ dt1 = pendulum.datetime(2000, 1, 1, 12, 45, 37)
+ dt2 = pendulum.datetime(2000, 1, 31, 12, 45, 37)
+
+ p = Interval(dt2, dt1)
+ r = list(p.range("days"))
+
+ assert len(r) == 31
+ assert_datetime(r[-1], 2000, 1, 1, 12, 45, 37)
+ assert_datetime(r[0], 2000, 1, 31, 12, 45, 37)
+
+
+def test_iter():
+ dt1 = pendulum.datetime(2000, 1, 1, 12, 45, 37)
+ dt2 = pendulum.datetime(2000, 1, 31, 12, 45, 37)
+
+ p = Interval(dt1, dt2)
+ i = 0 # noqa: SIM113 (suggests use of enumerate)
+ for dt in p:
+ assert isinstance(dt, pendulum.DateTime)
+ i += 1
+
+ assert i == 31
+
+
+def test_contains():
+ dt1 = pendulum.datetime(2000, 1, 1, 12, 45, 37)
+ dt2 = pendulum.datetime(2000, 1, 31, 12, 45, 37)
+
+ p = pendulum.interval(dt1, dt2)
+ dt = pendulum.datetime(2000, 1, 7)
+ assert dt in p
+
+
+def test_not_contains():
+ dt1 = pendulum.datetime(2000, 1, 1, 12, 45, 37)
+ dt2 = pendulum.datetime(2000, 1, 31, 12, 45, 37)
+
+ p = pendulum.interval(dt1, dt2)
+ dt = pendulum.datetime(2000, 1, 1, 11, 45, 37)
+ assert dt not in p
+
+
+def test_contains_with_datetime():
+ dt1 = pendulum.datetime(2000, 1, 1, 12, 45, 37)
+ dt2 = pendulum.datetime(2000, 1, 31, 12, 45, 37)
+
+ p = pendulum.interval(dt1, dt2)
+ dt = pendulum.datetime(2000, 1, 7)
+ assert dt in p
+
+
+def test_range_months_overflow():
+ dt1 = pendulum.datetime(2016, 1, 30, tz="America/Sao_Paulo")
+ dt2 = dt1.add(months=4)
+
+ p = pendulum.interval(dt1, dt2)
+ r = list(p.range("months"))
+
+ assert_datetime(r[0], 2016, 1, 30, 0, 0, 0)
+ assert_datetime(r[-1], 2016, 5, 30, 0, 0, 0)
+
+
+def test_range_with_dst():
+ dt1 = pendulum.datetime(2016, 10, 14, tz="America/Sao_Paulo")
+ dt2 = dt1.add(weeks=1)
+
+ p = pendulum.interval(dt1, dt2)
+ r = list(p.range("days"))
+
+ assert_datetime(r[0], 2016, 10, 14, 0, 0, 0)
+ assert_datetime(r[2], 2016, 10, 16, 1, 0, 0)
+ assert_datetime(r[-1], 2016, 10, 21, 0, 0, 0)
+
+
+def test_range_amount():
+ dt1 = pendulum.datetime(2016, 10, 14, tz="America/Sao_Paulo")
+ dt2 = dt1.add(weeks=1)
+
+ p = pendulum.interval(dt1, dt2)
+ r = list(p.range("days", 2))
+
+ assert len(r) == 4
+ assert_datetime(r[0], 2016, 10, 14, 0, 0, 0)
+ assert_datetime(r[1], 2016, 10, 16, 1, 0, 0)
+ assert_datetime(r[2], 2016, 10, 18, 0, 0, 0)
+ assert_datetime(r[3], 2016, 10, 20, 0, 0, 0)
diff --git a/tests/localization/__init__.py b/tests/localization/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/localization/__init__.py
diff --git a/tests/localization/test_cs.py b/tests/localization/test_cs.py
new file mode 100644
index 0000000..71b8340
--- /dev/null
+++ b/tests/localization/test_cs.py
@@ -0,0 +1,109 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "cs"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "pár vteřin zpět"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "pár vteřin zpět"
+
+ d = pendulum.now().subtract(seconds=20)
+ assert d.diff_for_humans(locale=locale) == "před 20 sekundami"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "před 1 minutou"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "před 2 minutami"
+
+ d = pendulum.now().subtract(minutes=5)
+ assert d.diff_for_humans(locale=locale) == "před 5 minutami"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "před 1 hodinou"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "před 2 hodinami"
+
+ d = pendulum.now().subtract(hours=5)
+ assert d.diff_for_humans(locale=locale) == "před 5 hodinami"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "před 1 dnem"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "před 2 dny"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "před 1 týdnem"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "před 2 týdny"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "před 1 měsícem"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "před 2 měsíci"
+
+ d = pendulum.now().subtract(months=5)
+ assert d.diff_for_humans(locale=locale) == "před 5 měsíci"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "před 1 rokem"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "před 2 lety"
+
+ d = pendulum.now().subtract(years=5)
+ assert d.diff_for_humans(locale=locale) == "před 5 lety"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "za pár vteřin"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "pár vteřin po"
+ assert d2.diff_for_humans(d, locale=locale) == "pár vteřin zpět"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "pár vteřin"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "pár vteřin"
+
+ d = pendulum.now().add(seconds=20)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "20 sekund po"
+ assert d2.diff_for_humans(d, locale=locale) == "20 sekund zpět"
+
+ d = pendulum.now().add(seconds=10)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, True, locale=locale) == "pár vteřin"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "11 sekund"
+
+
+def test_format():
+ d = pendulum.datetime(2016, 8, 29, 7, 3, 6, 123456)
+ assert d.format("dddd", locale=locale) == "pondělí"
+ assert d.format("ddd", locale=locale) == "po"
+ assert d.format("MMMM", locale=locale) == "srpna"
+ assert d.format("MMM", locale=locale) == "srp"
+ assert d.format("A", locale=locale) == "dop."
+ assert d.format("Qo", locale=locale) == "3."
+ assert d.format("Mo", locale=locale) == "8."
+ assert d.format("Do", locale=locale) == "29."
+
+ assert d.format("LT", locale=locale) == "7:03"
+ assert d.format("LTS", locale=locale) == "7:03:06"
+ assert d.format("L", locale=locale) == "29. 8. 2016"
+ assert d.format("LL", locale=locale) == "29. srpna, 2016"
+ assert d.format("LLL", locale=locale) == "29. srpna, 2016 7:03"
+ assert d.format("LLLL", locale=locale) == "pondělí, 29. srpna, 2016 7:03"
diff --git a/tests/localization/test_da.py b/tests/localization/test_da.py
new file mode 100644
index 0000000..b08adfe
--- /dev/null
+++ b/tests/localization/test_da.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "da"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 sekund siden"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 sekunder siden"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 minut siden"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 minutter siden"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 time siden"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 timer siden"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 dag siden"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 dage siden"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 uge siden"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 uger siden"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 måned siden"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 måneder siden"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 år siden"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 år siden"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "om 1 sekund"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "1 sekund efter"
+ assert d2.diff_for_humans(d, locale=locale) == "1 sekund før"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "1 sekund"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "2 sekunder"
diff --git a/tests/localization/test_de.py b/tests/localization/test_de.py
new file mode 100644
index 0000000..9c72b79
--- /dev/null
+++ b/tests/localization/test_de.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "de"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "vor 1 Sekunde"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "vor 2 Sekunden"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "vor 1 Minute"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "vor 2 Minuten"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "vor 1 Stunde"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "vor 2 Stunden"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "vor 1 Tag"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "vor 2 Tagen"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "vor 1 Woche"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "vor 2 Wochen"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "vor 1 Monat"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "vor 2 Monaten"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "vor 1 Jahr"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "vor 2 Jahren"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "in 1 Sekunde"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "1 Sekunde später"
+ assert d2.diff_for_humans(d, locale=locale) == "1 Sekunde zuvor"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "1 Sekunde"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "2 Sekunden"
diff --git a/tests/localization/test_es.py b/tests/localization/test_es.py
new file mode 100644
index 0000000..747ec6f
--- /dev/null
+++ b/tests/localization/test_es.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "es"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "hace unos segundos"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "hace unos segundos"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "hace 1 minuto"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "hace 2 minutos"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "hace 1 hora"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "hace 2 horas"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "hace 1 día"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "hace 2 días"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "hace 1 semana"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "hace 2 semanas"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "hace 1 mes"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "hace 2 meses"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "hace 1 año"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "hace 2 años"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "dentro de unos segundos"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "unos segundos después"
+ assert d2.diff_for_humans(d, locale=locale) == "unos segundos antes"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "unos segundos"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "unos segundos"
diff --git a/tests/localization/test_fa.py b/tests/localization/test_fa.py
new file mode 100644
index 0000000..39d2e4a
--- /dev/null
+++ b/tests/localization/test_fa.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "fa"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "1 ثانیه پیش"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "2 ثانیه پیش"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "1 دقیقه پیش"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "2 دقیقه پیش"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "1 ساعت پیش"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "2 ساعت پیش"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "1 روز پیش"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "2 روز پیش"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "1 هفته پیش"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "2 هفته پیش"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "1 ماه پیش"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "2 ماه پیش"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "1 سال پیش"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "2 سال پیش"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "1 ثانیه بعد"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "1 ثانیه پس از"
+ assert d2.diff_for_humans(d, locale=locale) == "1 ثانیه پیش از"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "1 ثانیه"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "2 ثانیه"
diff --git a/tests/localization/test_fo.py b/tests/localization/test_fo.py
new file mode 100644
index 0000000..f451553
--- /dev/null
+++ b/tests/localization/test_fo.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "fo"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "1 sekund síðan"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "2 sekund síðan"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "1 minutt síðan"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "2 minuttir síðan"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "1 tími síðan"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "2 tímar síðan"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "1 dagur síðan"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "2 dagar síðan"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "1 vika síðan"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "2 vikur síðan"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "1 mánað síðan"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "2 mánaðir síðan"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "1 ár síðan"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "2 ár síðan"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "um 1 sekund"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "1 sekund aftaná"
+ assert d2.diff_for_humans(d, locale=locale) == "1 sekund áðrenn"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "1 sekund"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "2 sekundir"
diff --git a/tests/localization/test_fr.py b/tests/localization/test_fr.py
new file mode 100644
index 0000000..1cfef5b
--- /dev/null
+++ b/tests/localization/test_fr.py
@@ -0,0 +1,84 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "fr"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "il y a quelques secondes"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "il y a quelques secondes"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "il y a 1 minute"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "il y a 2 minutes"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "il y a 1 heure"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "il y a 2 heures"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "il y a 1 jour"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "il y a 2 jours"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "il y a 1 semaine"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "il y a 2 semaines"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "il y a 1 mois"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "il y a 2 mois"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "il y a 1 an"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "il y a 2 ans"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "dans quelques secondes"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "quelques secondes après"
+ assert d2.diff_for_humans(d, locale=locale) == "quelques secondes avant"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "quelques secondes"
+ assert (
+ d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "quelques secondes"
+ )
+
+
+def test_format():
+ d = pendulum.datetime(2016, 8, 28, 7, 3, 6, 123456)
+ assert d.format("dddd", locale=locale) == "dimanche"
+ assert d.format("ddd", locale=locale) == "dim."
+ assert d.format("MMMM", locale=locale) == "août"
+ assert d.format("MMM", locale=locale) == "août"
+ assert d.format("A", locale=locale) == "AM"
+ assert d.format("Do", locale=locale) == "28e"
+
+ assert d.format("LT", locale=locale) == "07:03"
+ assert d.format("LTS", locale=locale) == "07:03:06"
+ assert d.format("L", locale=locale) == "28/08/2016"
+ assert d.format("LL", locale=locale) == "28 août 2016"
+ assert d.format("LLL", locale=locale) == "28 août 2016 07:03"
+ assert d.format("LLLL", locale=locale) == "dimanche 28 août 2016 07:03"
diff --git a/tests/localization/test_he.py b/tests/localization/test_he.py
new file mode 100644
index 0000000..6186ef2
--- /dev/null
+++ b/tests/localization/test_he.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "he"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "לפני כמה שניות"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "לפני כמה שניות"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "לפני דקה"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "לפני שתי דקות"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "לפני שעה"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "לפני שעתיים"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "לפני יום 1"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "לפני יומיים"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "לפני שבוע"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "לפני שבועיים"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "לפני חודש"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "לפני חודשיים"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "לפני שנה"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "לפני שנתיים"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "תוך כמה שניות"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "בעוד כמה שניות"
+ assert d2.diff_for_humans(d, locale=locale) == "כמה שניות קודם"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "כמה שניות"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "כמה שניות"
diff --git a/tests/localization/test_id.py b/tests/localization/test_id.py
new file mode 100644
index 0000000..3dd316c
--- /dev/null
+++ b/tests/localization/test_id.py
@@ -0,0 +1,68 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "id"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "beberapa detik yang lalu"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "beberapa detik yang lalu"
+
+ d = pendulum.now().subtract(seconds=21)
+ assert d.diff_for_humans(locale=locale) == "21 detik yang lalu"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "1 menit yang lalu"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "2 menit yang lalu"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "1 jam yang lalu"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "2 jam yang lalu"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "1 hari yang lalu"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "2 hari yang lalu"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "1 minggu yang lalu"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "2 minggu yang lalu"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "1 bulan yang lalu"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "2 bulan yang lalu"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "1 tahun yang lalu"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "2 tahun yang lalu"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "dalam beberapa detik"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "beberapa detik kemudian"
+ assert d2.diff_for_humans(d, locale=locale) == "beberapa detik yang lalu"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "beberapa detik"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "beberapa detik"
diff --git a/tests/localization/test_it.py b/tests/localization/test_it.py
new file mode 100644
index 0000000..1918a2b
--- /dev/null
+++ b/tests/localization/test_it.py
@@ -0,0 +1,85 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "it"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "alcuni secondi fa"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "alcuni secondi fa"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "1 minuto fa"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "2 minuti fa"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "1 ora fa"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "2 ore fa"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "1 giorno fa"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "2 giorni fa"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "1 settimana fa"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "2 settimane fa"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "1 mese fa"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "2 mesi fa"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "1 anno fa"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "2 anni fa"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "in alcuni secondi"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "alcuni secondi dopo"
+ assert d2.diff_for_humans(d, locale=locale) == "alcuni secondi prima"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "alcuni secondi"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "alcuni secondi"
+
+
+def test_format():
+ d = pendulum.datetime(2016, 8, 28, 7, 3, 6, 123456)
+ assert d.format("dddd", locale=locale) == "domenica"
+ assert d.format("ddd", locale=locale) == "dom"
+ assert d.format("MMMM", locale=locale) == "agosto"
+ assert d.format("MMM", locale=locale) == "ago"
+ assert d.format("A", locale=locale) == "AM"
+
+ assert d.format("LT", locale=locale) == "7:03"
+ assert d.format("LTS", locale=locale) == "7:03:06"
+ assert d.format("L", locale=locale) == "28/08/2016"
+ assert d.format("LL", locale=locale) == "28 agosto 2016"
+ assert d.format("LLL", locale=locale) == "28 agosto 2016 alle 7:03"
+ assert d.format("LLLL", locale=locale) == "domenica, 28 agosto 2016 alle 7:03"
+
+ assert d.format("Do", locale=locale) == "28°"
+ d = pendulum.datetime(2019, 1, 1, 7, 3, 6, 123456)
+ assert d.format("Do", locale=locale) == "1°"
diff --git a/tests/localization/test_ja.py b/tests/localization/test_ja.py
new file mode 100644
index 0000000..82457fd
--- /dev/null
+++ b/tests/localization/test_ja.py
@@ -0,0 +1,68 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "ja"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "数秒 前に"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "数秒 前に"
+
+ d = pendulum.now().subtract(seconds=21)
+ assert d.diff_for_humans(locale=locale) == "21 秒前"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "1 分前"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "2 分前"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "1 時間前"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "2 時間前"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "1 日前"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "2 日前"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "1 週間前"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "2 週間前"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "1 か月前"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "2 か月前"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "1 年前"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "2 年前"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "今から 数秒"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "数秒 後"
+ assert d2.diff_for_humans(d, locale=locale) == "数秒 前"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "数秒"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "数秒"
diff --git a/tests/localization/test_ko.py b/tests/localization/test_ko.py
new file mode 100644
index 0000000..e33ca25
--- /dev/null
+++ b/tests/localization/test_ko.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "ko"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "1초 전"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "2초 전"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "1분 전"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "2분 전"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "1시간 전"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "2시간 전"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "1일 전"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "2일 전"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "1주 전"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "2주 전"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "1개월 전"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "2개월 전"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "1년 전"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "2년 전"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "1초 후"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "1초 뒤"
+ assert d2.diff_for_humans(d, locale=locale) == "1초 앞"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "1초"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "2초"
diff --git a/tests/localization/test_lt.py b/tests/localization/test_lt.py
new file mode 100644
index 0000000..71de1ac
--- /dev/null
+++ b/tests/localization/test_lt.py
@@ -0,0 +1,68 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "lt"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "prieš 1 sekundę"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "prieš 2 sekundes"
+
+ d = pendulum.now().subtract(seconds=21)
+ assert d.diff_for_humans(locale=locale) == "prieš 21 sekundę"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "prieš 1 minutę"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "prieš 2 minutes"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "prieš 1 valandą"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "prieš 2 valandas"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "prieš 1 dieną"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "prieš 2 dienas"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "prieš 1 savaitę"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "prieš 2 savaites"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "prieš 1 mėnesį"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "prieš 2 mėnesius"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "prieš 1 metus"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "prieš 2 metus"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "po 1 sekundės"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "po 1 sekundės"
+ assert d2.diff_for_humans(d, locale=locale) == "1 sekundę nuo dabar"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "1 sekundė"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "2 sekundės"
diff --git a/tests/localization/test_nb.py b/tests/localization/test_nb.py
new file mode 100644
index 0000000..3f696e5
--- /dev/null
+++ b/tests/localization/test_nb.py
@@ -0,0 +1,84 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "nb"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 sekund siden"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 sekunder siden"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 minutt siden"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 minutter siden"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 time siden"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 timer siden"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 dag siden"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 dager siden"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 uke siden"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 uker siden"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 måned siden"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 måneder siden"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 år siden"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 år siden"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "om 1 sekund"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "1 sekund etter"
+ assert d2.diff_for_humans(d, locale=locale) == "1 sekund før"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "1 sekund"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "2 sekunder"
+
+
+def test_format():
+ d = pendulum.datetime(2016, 8, 28, 7, 3, 6, 123456)
+ assert d.format("dddd", locale=locale) == "søndag"
+ assert d.format("ddd", locale=locale) == "søn."
+ assert d.format("MMMM", locale=locale) == "august"
+ assert d.format("MMM", locale=locale) == "aug."
+ assert d.format("A", locale=locale) == "a.m."
+ assert d.format("Qo", locale=locale) == "3."
+ assert d.format("Mo", locale=locale) == "8."
+ assert d.format("Do", locale=locale) == "28."
+
+ assert d.format("LT", locale=locale) == "07:03"
+ assert d.format("LTS", locale=locale) == "07:03:06"
+ assert d.format("L", locale=locale) == "28.08.2016"
+ assert d.format("LL", locale=locale) == "28. august 2016"
+ assert d.format("LLL", locale=locale) == "28. august 2016 07:03"
+ assert d.format("LLLL", locale=locale) == "søndag 28. august 2016 07:03"
diff --git a/tests/localization/test_nl.py b/tests/localization/test_nl.py
new file mode 100644
index 0000000..68227ec
--- /dev/null
+++ b/tests/localization/test_nl.py
@@ -0,0 +1,83 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "nl"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "enkele seconden geleden"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "enkele seconden geleden"
+
+ d = pendulum.now().subtract(seconds=22)
+ assert d.diff_for_humans(locale=locale) == "22 seconden geleden"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "1 minuut geleden"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "2 minuten geleden"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "1 uur geleden"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "2 uur geleden"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "1 dag geleden"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "2 dagen geleden"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "1 week geleden"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "2 weken geleden"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "1 maand geleden"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "2 maanden geleden"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "1 jaar geleden"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "2 jaar geleden"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "over enkele seconden"
+
+ d = pendulum.now().add(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "over 1 week"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "enkele seconden later"
+ assert d2.diff_for_humans(d, locale=locale) == "enkele seconden eerder"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "enkele seconden"
+ assert (
+ d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "enkele seconden"
+ )
+
+
+def test_format():
+ d = pendulum.datetime(2016, 8, 28, 7, 3, 6, 123456)
+ assert d.format("dddd", locale=locale) == "zondag"
+ assert d.format("ddd", locale=locale) == "zo"
+ assert d.format("MMMM", locale=locale) == "augustus"
+ assert d.format("MMM", locale=locale) == "aug."
+ assert d.format("A", locale=locale) == "a.m."
+ assert d.format("Do", locale=locale) == "28e"
diff --git a/tests/localization/test_nn.py b/tests/localization/test_nn.py
new file mode 100644
index 0000000..d4b8099
--- /dev/null
+++ b/tests/localization/test_nn.py
@@ -0,0 +1,84 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "nn"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 sekund sidan"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 sekund sidan"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 minutt sidan"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 minutt sidan"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 time sidan"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 timar sidan"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 dag sidan"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 dagar sidan"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 veke sidan"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 veker sidan"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 månad sidan"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 månadar sidan"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "for 1 år sidan"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "for 2 år sidan"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "om 1 sekund"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "1 sekund etter"
+ assert d2.diff_for_humans(d, locale=locale) == "1 sekund før"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "1 sekund"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "2 sekund"
+
+
+def test_format():
+ d = pendulum.datetime(2016, 8, 29, 7, 3, 6, 123456)
+ assert d.format("dddd", locale=locale) == "måndag"
+ assert d.format("ddd", locale=locale) == "mån."
+ assert d.format("MMMM", locale=locale) == "august"
+ assert d.format("MMM", locale=locale) == "aug."
+ assert d.format("A", locale=locale) == "formiddag"
+ assert d.format("Qo", locale=locale) == "3."
+ assert d.format("Mo", locale=locale) == "8."
+ assert d.format("Do", locale=locale) == "29."
+
+ assert d.format("LT", locale=locale) == "07:03"
+ assert d.format("LTS", locale=locale) == "07:03:06"
+ assert d.format("L", locale=locale) == "29.08.2016"
+ assert d.format("LL", locale=locale) == "29. august 2016"
+ assert d.format("LLL", locale=locale) == "29. august 2016 07:03"
+ assert d.format("LLLL", locale=locale) == "måndag 29. august 2016 07:03"
diff --git a/tests/localization/test_pl.py b/tests/localization/test_pl.py
new file mode 100644
index 0000000..2b6e707
--- /dev/null
+++ b/tests/localization/test_pl.py
@@ -0,0 +1,109 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "pl"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "kilka sekund temu"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "kilka sekund temu"
+
+ d = pendulum.now().subtract(seconds=20)
+ assert d.diff_for_humans(locale=locale) == "20 sekund temu"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "1 minutę temu"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "2 minuty temu"
+
+ d = pendulum.now().subtract(minutes=5)
+ assert d.diff_for_humans(locale=locale) == "5 minut temu"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "1 godzinę temu"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "2 godziny temu"
+
+ d = pendulum.now().subtract(hours=5)
+ assert d.diff_for_humans(locale=locale) == "5 godzin temu"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "1 dzień temu"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "2 dni temu"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "1 tydzień temu"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "2 tygodnie temu"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "1 miesiąc temu"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "2 miesiące temu"
+
+ d = pendulum.now().subtract(months=5)
+ assert d.diff_for_humans(locale=locale) == "5 miesięcy temu"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "1 rok temu"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "2 lata temu"
+
+ d = pendulum.now().subtract(years=5)
+ assert d.diff_for_humans(locale=locale) == "5 lat temu"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "za kilka sekund"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "kilka sekund po"
+ assert d2.diff_for_humans(d, locale=locale) == "kilka sekund przed"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "kilka sekund"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "kilka sekund"
+
+ d = pendulum.now().add(seconds=20)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "20 sekund po"
+ assert d2.diff_for_humans(d, locale=locale) == "20 sekund przed"
+
+ d = pendulum.now().add(seconds=10)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, True, locale=locale) == "kilka sekund"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "11 sekund"
+
+
+def test_format():
+ d = pendulum.datetime(2016, 8, 29, 7, 3, 6, 123456)
+ assert d.format("dddd", locale=locale) == "poniedziałek"
+ assert d.format("ddd", locale=locale) == "pon."
+ assert d.format("MMMM", locale=locale) == "sierpnia"
+ assert d.format("MMM", locale=locale) == "sie"
+ assert d.format("A", locale=locale) == "AM"
+ assert d.format("Qo", locale=locale) == "3"
+ assert d.format("Mo", locale=locale) == "8"
+ assert d.format("Do", locale=locale) == "29"
+
+ assert d.format("LT", locale=locale) == "07:03"
+ assert d.format("LTS", locale=locale) == "07:03:06"
+ assert d.format("L", locale=locale) == "29.08.2016"
+ assert d.format("LL", locale=locale) == "29 sierpnia 2016"
+ assert d.format("LLL", locale=locale) == "29 sierpnia 2016 07:03"
+ assert d.format("LLLL", locale=locale) == "poniedziałek, 29 sierpnia 2016 07:03"
diff --git a/tests/localization/test_ru.py b/tests/localization/test_ru.py
new file mode 100644
index 0000000..be0e645
--- /dev/null
+++ b/tests/localization/test_ru.py
@@ -0,0 +1,86 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "ru"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "1 секунду назад"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "2 секунды назад"
+
+ d = pendulum.now().subtract(seconds=5)
+ assert d.diff_for_humans(locale=locale) == "5 секунд назад"
+
+ d = pendulum.now().subtract(seconds=21)
+ assert d.diff_for_humans(locale=locale) == "21 секунду назад"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "1 минуту назад"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "2 минуты назад"
+
+ d = pendulum.now().subtract(minutes=5)
+ assert d.diff_for_humans(locale=locale) == "5 минут назад"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "1 час назад"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "2 часа назад"
+
+ d = pendulum.now().subtract(hours=5)
+ assert d.diff_for_humans(locale=locale) == "5 часов назад"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "1 день назад"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "2 дня назад"
+
+ d = pendulum.now().subtract(days=5)
+ assert d.diff_for_humans(locale=locale) == "5 дней назад"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "1 неделю назад"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "2 недели назад"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "1 месяц назад"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "2 месяца назад"
+
+ d = pendulum.now().subtract(months=5)
+ assert d.diff_for_humans(locale=locale) == "5 месяцев назад"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "1 год назад"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "2 года назад"
+
+ d = pendulum.now().subtract(years=5)
+ assert d.diff_for_humans(locale=locale) == "5 лет назад"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "через 1 секунду"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "1 секунда после"
+ assert d2.diff_for_humans(d, locale=locale) == "1 секунда до"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "1 секунда"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "2 секунды"
diff --git a/tests/localization/test_sk.py b/tests/localization/test_sk.py
new file mode 100644
index 0000000..5553e7f
--- /dev/null
+++ b/tests/localization/test_sk.py
@@ -0,0 +1,112 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "sk"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "pred 1 sekundou"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "o 1 sekundu"
+
+ d = pendulum.now().add(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "o 2 sekundy"
+
+ d = pendulum.now().add(seconds=5)
+ assert d.diff_for_humans(locale=locale) == "o 5 sekúnd"
+
+ d = pendulum.now().subtract(seconds=20)
+ assert d.diff_for_humans(locale=locale) == "pred 20 sekundami"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "pred 1 minútou"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "pred 2 minútami"
+
+ d = pendulum.now().add(minutes=5)
+ assert d.diff_for_humans(locale=locale) == "o 5 minút"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "pred 1 hodinou"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "pred 2 hodinami"
+
+ d = pendulum.now().subtract(hours=5)
+ assert d.diff_for_humans(locale=locale) == "pred 5 hodinami"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "pred 1 dňom"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "pred 2 dňami"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "pred 1 týždňom"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "pred 2 týždňami"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "pred 1 mesiacom"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "pred 2 mesiacmi"
+
+ d = pendulum.now().subtract(months=5)
+ assert d.diff_for_humans(locale=locale) == "pred 5 mesiacmi"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "pred 1 rokom"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "pred 2 rokmi"
+
+ d = pendulum.now().subtract(years=5)
+ assert d.diff_for_humans(locale=locale) == "pred 5 rokmi"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "1 sekunda po"
+ assert d2.diff_for_humans(d, locale=locale) == "1 sekunda pred"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "1 sekunda"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "2 sekundy"
+
+ d = pendulum.now().add(seconds=20)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "20 sekúnd po"
+ assert d2.diff_for_humans(d, locale=locale) == "20 sekúnd pred"
+
+ d = pendulum.now().add(seconds=10)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, True, locale=locale) == "10 sekúnd"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "11 sekúnd"
+
+
+def test_format():
+ d = pendulum.datetime(2016, 8, 29, 7, 3, 6, 123456)
+ assert d.format("dddd", locale=locale) == "pondelok"
+ assert d.format("ddd", locale=locale) == "po"
+ assert d.format("MMMM", locale=locale) == "augusta"
+ assert d.format("MMM", locale=locale) == "aug"
+ assert d.format("A", locale=locale) == "AM"
+ assert d.format("Qo", locale=locale) == "3"
+ assert d.format("Mo", locale=locale) == "8"
+ assert d.format("Do", locale=locale) == "29"
+
+ assert d.format("LT", locale=locale) == "07:03"
+ assert d.format("LTS", locale=locale) == "07:03:06"
+ assert d.format("L", locale=locale) == "29.08.2016"
+ assert d.format("LL", locale=locale) == "29. augusta 2016"
+ assert d.format("LLL", locale=locale) == "29. augusta 2016 07:03"
+ assert d.format("LLLL", locale=locale) == "pondelok, 29. augusta 2016 07:03"
diff --git a/tests/localization/test_sv.py b/tests/localization/test_sv.py
new file mode 100644
index 0000000..e0e4e65
--- /dev/null
+++ b/tests/localization/test_sv.py
@@ -0,0 +1,86 @@
+from __future__ import annotations
+
+import pendulum
+
+locale = "sv"
+
+
+def test_diff_for_humans():
+ with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
+ diff_for_humans()
+
+
+def diff_for_humans():
+ d = pendulum.now().subtract(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "för 1 sekund sedan"
+
+ d = pendulum.now().subtract(seconds=2)
+ assert d.diff_for_humans(locale=locale) == "för 2 sekunder sedan"
+
+ d = pendulum.now().subtract(seconds=5)
+ assert d.diff_for_humans(locale=locale) == "för 5 sekunder sedan"
+
+ d = pendulum.now().subtract(seconds=21)
+ assert d.diff_for_humans(locale=locale) == "för 21 sekunder sedan"
+
+ d = pendulum.now().subtract(minutes=1)
+ assert d.diff_for_humans(locale=locale) == "för 1 minut sedan"
+
+ d = pendulum.now().subtract(minutes=2)
+ assert d.diff_for_humans(locale=locale) == "för 2 minuter sedan"
+
+ d = pendulum.now().subtract(minutes=5)
+ assert d.diff_for_humans(locale=locale) == "för 5 minuter sedan"
+
+ d = pendulum.now().subtract(hours=1)
+ assert d.diff_for_humans(locale=locale) == "för 1 timme sedan"
+
+ d = pendulum.now().subtract(hours=2)
+ assert d.diff_for_humans(locale=locale) == "för 2 timmar sedan"
+
+ d = pendulum.now().subtract(hours=5)
+ assert d.diff_for_humans(locale=locale) == "för 5 timmar sedan"
+
+ d = pendulum.now().subtract(days=1)
+ assert d.diff_for_humans(locale=locale) == "för 1 dag sedan"
+
+ d = pendulum.now().subtract(days=2)
+ assert d.diff_for_humans(locale=locale) == "för 2 dagar sedan"
+
+ d = pendulum.now().subtract(days=5)
+ assert d.diff_for_humans(locale=locale) == "för 5 dagar sedan"
+
+ d = pendulum.now().subtract(weeks=1)
+ assert d.diff_for_humans(locale=locale) == "för 1 vecka sedan"
+
+ d = pendulum.now().subtract(weeks=2)
+ assert d.diff_for_humans(locale=locale) == "för 2 veckor sedan"
+
+ d = pendulum.now().subtract(months=1)
+ assert d.diff_for_humans(locale=locale) == "för 1 månad sedan"
+
+ d = pendulum.now().subtract(months=2)
+ assert d.diff_for_humans(locale=locale) == "för 2 månader sedan"
+
+ d = pendulum.now().subtract(months=5)
+ assert d.diff_for_humans(locale=locale) == "för 5 månader sedan"
+
+ d = pendulum.now().subtract(years=1)
+ assert d.diff_for_humans(locale=locale) == "för 1 år sedan"
+
+ d = pendulum.now().subtract(years=2)
+ assert d.diff_for_humans(locale=locale) == "för 2 år sedan"
+
+ d = pendulum.now().subtract(years=5)
+ assert d.diff_for_humans(locale=locale) == "för 5 år sedan"
+
+ d = pendulum.now().add(seconds=1)
+ assert d.diff_for_humans(locale=locale) == "om 1 sekund"
+
+ d = pendulum.now().add(seconds=1)
+ d2 = pendulum.now()
+ assert d.diff_for_humans(d2, locale=locale) == "1 sekund efter"
+ assert d2.diff_for_humans(d, locale=locale) == "1 sekund innan"
+
+ assert d.diff_for_humans(d2, True, locale=locale) == "1 sekund"
+ assert d2.diff_for_humans(d.add(seconds=1), True, locale=locale) == "2 sekunder"
diff --git a/tests/parsing/__init__.py b/tests/parsing/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/parsing/__init__.py
diff --git a/tests/parsing/test_parse_iso8601.py b/tests/parsing/test_parse_iso8601.py
new file mode 100644
index 0000000..0047791
--- /dev/null
+++ b/tests/parsing/test_parse_iso8601.py
@@ -0,0 +1,465 @@
+from __future__ import annotations
+
+from datetime import date
+from datetime import datetime
+from datetime import time
+
+import pytest
+
+from pendulum.parsing import parse_iso8601
+
+try:
+ from pendulum.parsing._extension import TZFixedOffset as FixedTimezone
+except ImportError:
+ from pendulum.tz.timezone import FixedTimezone
+
+
+def test_parse_iso8601():
+ # Date
+ assert date(2016, 1, 1) == parse_iso8601("2016")
+ assert date(2016, 10, 1) == parse_iso8601("2016-10")
+ assert date(2016, 10, 6) == parse_iso8601("2016-10-06")
+ assert date(2016, 10, 6) == parse_iso8601("20161006")
+
+ # Time
+ assert time(20, 16, 10, 0) == parse_iso8601("201610")
+
+ # Datetime
+ assert datetime(2016, 10, 6, 12, 34, 56, 123456) == parse_iso8601(
+ "2016-10-06T12:34:56.123456"
+ )
+ assert datetime(2016, 10, 6, 12, 34, 56, 123000) == parse_iso8601(
+ "2016-10-06T12:34:56.123"
+ )
+ assert datetime(2016, 10, 6, 12, 34, 56, 123) == parse_iso8601(
+ "2016-10-06T12:34:56.000123"
+ )
+ assert datetime(2016, 10, 6, 12, 0, 0, 0) == parse_iso8601("2016-10-06T12")
+ assert datetime(2016, 10, 6, 12, 34, 56, 0) == parse_iso8601("2016-10-06T123456")
+ assert datetime(2016, 10, 6, 12, 34, 56, 123456) == parse_iso8601(
+ "2016-10-06T123456.123456"
+ )
+ assert datetime(2016, 10, 6, 12, 34, 56, 123456) == parse_iso8601(
+ "20161006T123456.123456"
+ )
+ assert datetime(2016, 10, 6, 12, 34, 56, 123456) == parse_iso8601(
+ "20161006 123456.123456"
+ )
+
+ # Datetime with offset
+ assert datetime(
+ 2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(19800)
+ ) == parse_iso8601("2016-10-06T12:34:56.123456+05:30")
+ assert datetime(
+ 2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(19800)
+ ) == parse_iso8601("2016-10-06T12:34:56.123456+0530")
+ assert datetime(
+ 2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(-19800)
+ ) == parse_iso8601("2016-10-06T12:34:56.123456-05:30")
+ assert datetime(
+ 2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(-19800)
+ ) == parse_iso8601("2016-10-06T12:34:56.123456-0530")
+ assert datetime(
+ 2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(18000)
+ ) == parse_iso8601("2016-10-06T12:34:56.123456+05")
+ assert datetime(
+ 2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(-18000)
+ ) == parse_iso8601("2016-10-06T12:34:56.123456-05")
+ assert datetime(
+ 2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(-18000)
+ ) == parse_iso8601("20161006T123456,123456-05")
+ assert datetime(
+ 2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(+19800)
+ ) == parse_iso8601("2016-10-06T12:34:56.123456789+05:30")
+
+ # Ordinal date
+ assert date(2012, 1, 7) == parse_iso8601("2012-007")
+ assert date(2012, 1, 7) == parse_iso8601("2012007")
+ assert date(2017, 3, 20) == parse_iso8601("2017-079")
+
+ # Week date
+ assert date(2012, 1, 30) == parse_iso8601("2012-W05")
+ assert date(2008, 9, 27) == parse_iso8601("2008-W39-6")
+ assert date(2010, 1, 3) == parse_iso8601("2009-W53-7")
+ assert date(2008, 12, 29) == parse_iso8601("2009-W01-1")
+
+ # Week date wth time
+ assert datetime(2008, 9, 27, 9, 0, 0, 0) == parse_iso8601("2008-W39-6T09")
+
+
+def test_parse_ios8601_invalid():
+ # Invalid month
+ with pytest.raises(ValueError):
+ parse_iso8601("20161306T123456")
+
+ # Invalid day
+ with pytest.raises(ValueError):
+ parse_iso8601("20161033T123456")
+
+ # Invalid day for month
+ with pytest.raises(ValueError):
+ parse_iso8601("20161131T123456")
+
+ # Invalid hour
+ with pytest.raises(ValueError):
+ parse_iso8601("20161006T243456")
+
+ # Invalid minute
+ with pytest.raises(ValueError):
+ parse_iso8601("20161006T126056")
+
+ # Invalid second
+ with pytest.raises(ValueError):
+ parse_iso8601("20161006T123460")
+
+ # Extraneous separator
+ with pytest.raises(ValueError):
+ parse_iso8601("20140203 04:05:.123456")
+ with pytest.raises(ValueError):
+ parse_iso8601("2009-05-19 14:")
+
+ # Invalid ordinal
+ with pytest.raises(ValueError):
+ parse_iso8601("2009367")
+ with pytest.raises(ValueError):
+ parse_iso8601("2009-367")
+ with pytest.raises(ValueError):
+ parse_iso8601("2015-366")
+ with pytest.raises(ValueError):
+ parse_iso8601("2015-000")
+
+ # Invalid date
+ with pytest.raises(ValueError):
+ parse_iso8601("2009-")
+
+ # Invalid time
+ with pytest.raises(ValueError):
+ parse_iso8601("2009-05-19T14:3924")
+ with pytest.raises(ValueError):
+ parse_iso8601("2010-02-18T16.5:23.35:48")
+ with pytest.raises(ValueError):
+ parse_iso8601("2010-02-18T16:23.35:48.45")
+ with pytest.raises(ValueError):
+ parse_iso8601("2010-02-18T16:23.33.600")
+
+ # Invalid offset
+ with pytest.raises(ValueError):
+ parse_iso8601("2009-05-19 14:39:22+063")
+ with pytest.raises(ValueError):
+ parse_iso8601("2009-05-19 14:39:22+06a00")
+ with pytest.raises(ValueError):
+ parse_iso8601("2009-05-19 14:39:22+0:6:00")
+
+ # Missing time separator
+ with pytest.raises(ValueError):
+ parse_iso8601("2009-05-1914:39")
+
+ # Invalid week date
+ with pytest.raises(ValueError):
+ parse_iso8601("2012-W63")
+ with pytest.raises(ValueError):
+ parse_iso8601("2012-W12-9")
+ with pytest.raises(ValueError):
+ parse_iso8601("2012W12-3") # Missing separator
+ with pytest.raises(ValueError):
+ parse_iso8601("2012-W123") # Missing separator
+
+
+def test_parse_ios8601_duration():
+ text = "P2Y3M4DT5H6M7S"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 2
+ assert parsed.months == 3
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 4
+ assert parsed.hours == 5
+ assert parsed.minutes == 6
+ assert parsed.remaining_seconds == 7
+ assert parsed.microseconds == 0
+
+ text = "P1Y2M3DT4H5M6.5S"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 1
+ assert parsed.months == 2
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 3
+ assert parsed.hours == 4
+ assert parsed.minutes == 5
+ assert parsed.remaining_seconds == 6
+ assert parsed.microseconds == 500000
+
+ text = "P1Y2M3DT4H5M6,5S"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 1
+ assert parsed.months == 2
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 3
+ assert parsed.hours == 4
+ assert parsed.minutes == 5
+ assert parsed.remaining_seconds == 6
+ assert parsed.microseconds == 500000
+
+ text = "P1Y2M3D"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 1
+ assert parsed.months == 2
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 3
+ assert parsed.hours == 0
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1Y2M3.5D"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 1
+ assert parsed.months == 2
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 3
+ assert parsed.hours == 12
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1Y2M3,5D"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 1
+ assert parsed.months == 2
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 3
+ assert parsed.hours == 12
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "PT4H54M6.5S"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 0
+ assert parsed.hours == 4
+ assert parsed.minutes == 54
+ assert parsed.remaining_seconds == 6
+ assert parsed.microseconds == 500000
+
+ text = "PT4H54M6,5S"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 0
+ assert parsed.hours == 4
+ assert parsed.minutes == 54
+ assert parsed.remaining_seconds == 6
+ assert parsed.microseconds == 500000
+
+ text = "P1Y"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 1
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 0
+ assert parsed.hours == 0
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1.5Y"
+ with pytest.raises(ValueError):
+ parse_iso8601(text)
+
+ text = "P1,5Y"
+ with pytest.raises(ValueError):
+ parse_iso8601(text)
+
+ text = "P1M"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 1
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 0
+ assert parsed.hours == 0
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1.5M"
+ with pytest.raises(ValueError):
+ parse_iso8601(text)
+
+ text = "P1,5M"
+ with pytest.raises(ValueError):
+ parse_iso8601(text)
+
+ text = "P1W"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 1
+ assert parsed.remaining_days == 0
+ assert parsed.hours == 0
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1.5W"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 1
+ assert parsed.remaining_days == 3
+ assert parsed.hours == 12
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1,5W"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 1
+ assert parsed.remaining_days == 3
+ assert parsed.hours == 12
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1D"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 1
+ assert parsed.hours == 0
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1.5D"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 1
+ assert parsed.hours == 12
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1,5D"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 1
+ assert parsed.hours == 12
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "PT1H"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 0
+ assert parsed.hours == 1
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "PT1.5H"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 0
+ assert parsed.hours == 1
+ assert parsed.minutes == 30
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "PT1,5H"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 0
+ assert parsed.hours == 1
+ assert parsed.minutes == 30
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ # Double digit with 0
+ text = "P2Y30M4DT5H6M7S"
+ parsed = parse_iso8601(text)
+
+ assert parsed.years == 2
+ assert parsed.months == 30
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 4
+ assert parsed.hours == 5
+ assert parsed.minutes == 6
+ assert parsed.remaining_seconds == 7
+ assert parsed.microseconds == 0
+
+ # No P operator
+ with pytest.raises(ValueError):
+ parse_iso8601("2Y3M4DT5H6M7S")
+
+ # Week and other units combined
+ with pytest.raises(ValueError):
+ parse_iso8601("P1Y2W")
+
+ # Invalid units order
+ with pytest.raises(ValueError):
+ parse_iso8601("P1S")
+
+ with pytest.raises(ValueError):
+ parse_iso8601("P1D1S")
+
+ with pytest.raises(ValueError):
+ parse_iso8601("1Y2M3D1SPT1M")
+
+ with pytest.raises(ValueError):
+ parse_iso8601("P1Y2M3D2MT1S")
+
+ with pytest.raises(ValueError):
+ parse_iso8601("P2M3D1ST1Y1M")
+
+ with pytest.raises(ValueError):
+ parse_iso8601("P1Y2M2MT3D1S")
+
+ with pytest.raises(ValueError):
+ parse_iso8601("P1D1Y1M")
+
+ with pytest.raises(ValueError):
+ parse_iso8601("PT1S1H")
+
+ # Invalid
+ with pytest.raises(ValueError):
+ parse_iso8601("P1Dasdfasdf")
+
+ # Invalid fractional
+ with pytest.raises(ValueError):
+ parse_iso8601("P2Y3M4DT5.5H6M7S")
diff --git a/tests/parsing/test_parsing.py b/tests/parsing/test_parsing.py
new file mode 100644
index 0000000..35dcf86
--- /dev/null
+++ b/tests/parsing/test_parsing.py
@@ -0,0 +1,687 @@
+from __future__ import annotations
+
+import datetime
+
+import pytest
+
+import pendulum
+
+from pendulum.parsing import ParserError
+from pendulum.parsing import parse
+
+
+def test_y():
+ text = "2016"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2016
+ assert parsed.month == 1
+ assert parsed.day == 1
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+
+def test_ym():
+ text = "2016-10"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2016
+ assert parsed.month == 10
+ assert parsed.day == 1
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+
+def test_ymd():
+ text = "2016-10-06"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2016
+ assert parsed.month == 10
+ assert parsed.day == 6
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+
+def test_ymd_one_character():
+ text = "2016-2-6"
+
+ parsed = parse(text, strict=False)
+
+ assert parsed.year == 2016
+ assert parsed.month == 2
+ assert parsed.day == 6
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+
+def test_ymd_hms():
+ text = "2016-10-06 12:34:56"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2016
+ assert parsed.month == 10
+ assert parsed.day == 6
+ assert parsed.hour == 12
+ assert parsed.minute == 34
+ assert parsed.second == 56
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "2016-10-06 12:34:56.123456"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2016
+ assert parsed.month == 10
+ assert parsed.day == 6
+ assert parsed.hour == 12
+ assert parsed.minute == 34
+ assert parsed.second == 56
+ assert parsed.microsecond == 123456
+ assert parsed.tzinfo is None
+
+
+def test_rfc_3339():
+ text = "2016-10-06T12:34:56+05:30"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2016
+ assert parsed.month == 10
+ assert parsed.day == 6
+ assert parsed.hour == 12
+ assert parsed.minute == 34
+ assert parsed.second == 56
+ assert parsed.microsecond == 0
+ assert parsed.utcoffset().total_seconds() == 19800
+
+
+def test_rfc_3339_extended():
+ text = "2016-10-06T12:34:56.123456+05:30"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2016
+ assert parsed.month == 10
+ assert parsed.day == 6
+ assert parsed.hour == 12
+ assert parsed.minute == 34
+ assert parsed.second == 56
+ assert parsed.microsecond == 123456
+ assert parsed.utcoffset().total_seconds() == 19800
+
+ text = "2016-10-06T12:34:56.000123+05:30"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2016
+ assert parsed.month == 10
+ assert parsed.day == 6
+ assert parsed.hour == 12
+ assert parsed.minute == 34
+ assert parsed.second == 56
+ assert parsed.microsecond == 123
+ assert parsed.utcoffset().total_seconds() == 19800
+
+
+def test_rfc_3339_extended_nanoseconds():
+ text = "2016-10-06T12:34:56.123456789+05:30"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2016
+ assert parsed.month == 10
+ assert parsed.day == 6
+ assert parsed.hour == 12
+ assert parsed.minute == 34
+ assert parsed.second == 56
+ assert parsed.microsecond == 123456
+ assert parsed.utcoffset().total_seconds() == 19800
+
+
+def test_iso_8601_date():
+ text = "2012"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2012
+ assert parsed.month == 1
+ assert parsed.day == 1
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "2012-05-03"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2012
+ assert parsed.month == 5
+ assert parsed.day == 3
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "20120503"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2012
+ assert parsed.month == 5
+ assert parsed.day == 3
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "2012-05"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2012
+ assert parsed.month == 5
+ assert parsed.day == 1
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+
+def test_iso8601_datetime():
+ text = "2016-10-01T14"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2016
+ assert parsed.month == 10
+ assert parsed.day == 1
+ assert parsed.hour == 14
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "2016-10-01T14:30"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2016
+ assert parsed.month == 10
+ assert parsed.day == 1
+ assert parsed.hour == 14
+ assert parsed.minute == 30
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "20161001T14"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2016
+ assert parsed.month == 10
+ assert parsed.day == 1
+ assert parsed.hour == 14
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "20161001T1430"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2016
+ assert parsed.month == 10
+ assert parsed.day == 1
+ assert parsed.hour == 14
+ assert parsed.minute == 30
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "20161001T1430+0530"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2016
+ assert parsed.month == 10
+ assert parsed.day == 1
+ assert parsed.hour == 14
+ assert parsed.minute == 30
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.utcoffset().total_seconds() == 19800
+
+ text = "20161001T1430,4+0530"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2016
+ assert parsed.month == 10
+ assert parsed.day == 1
+ assert parsed.hour == 14
+ assert parsed.minute == 30
+ assert parsed.second == 0
+ assert parsed.microsecond == 400000
+ assert parsed.utcoffset().total_seconds() == 19800
+
+ text = "2008-09-03T20:56:35.450686+01"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2008
+ assert parsed.month == 9
+ assert parsed.day == 3
+ assert parsed.hour == 20
+ assert parsed.minute == 56
+ assert parsed.second == 35
+ assert parsed.microsecond == 450686
+ assert parsed.utcoffset().total_seconds() == 3600
+
+
+def test_iso8601_week_number():
+ text = "2012-W05"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2012
+ assert parsed.month == 1
+ assert parsed.day == 30
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "2012W05"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2012
+ assert parsed.month == 1
+ assert parsed.day == 30
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ # Long Year
+ text = "2015W53"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2015
+ assert parsed.month == 12
+ assert parsed.day == 28
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "2012-W05-5"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2012
+ assert parsed.month == 2
+ assert parsed.day == 3
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "2012W055"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2012
+ assert parsed.month == 2
+ assert parsed.day == 3
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "2009-W53-7"
+ parsed = parse(text)
+
+ assert parsed.year == 2010
+ assert parsed.month == 1
+ assert parsed.day == 3
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "2009-W01-1"
+ parsed = parse(text)
+
+ assert parsed.year == 2008
+ assert parsed.month == 12
+ assert parsed.day == 29
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+
+def test_iso8601_week_number_with_time():
+ text = "2012-W05T09"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2012
+ assert parsed.month == 1
+ assert parsed.day == 30
+ assert parsed.hour == 9
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "2012W05T09"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2012
+ assert parsed.month == 1
+ assert parsed.day == 30
+ assert parsed.hour == 9
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "2012-W05-5T09"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2012
+ assert parsed.month == 2
+ assert parsed.day == 3
+ assert parsed.hour == 9
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "2012W055T09"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2012
+ assert parsed.month == 2
+ assert parsed.day == 3
+ assert parsed.hour == 9
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+
+def test_iso8601_ordinal():
+ text = "2012-007"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2012
+ assert parsed.month == 1
+ assert parsed.day == 7
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "2012007"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2012
+ assert parsed.month == 1
+ assert parsed.day == 7
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+
+def test_iso8601_time():
+ now = pendulum.datetime(2015, 11, 12)
+
+ text = "201205"
+
+ parsed = parse(text, now=now)
+
+ assert parsed.year == 2015
+ assert parsed.month == 11
+ assert parsed.day == 12
+ assert parsed.hour == 20
+ assert parsed.minute == 12
+ assert parsed.second == 5
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "20:12:05"
+
+ parsed = parse(text, now=now)
+
+ assert parsed.year == 2015
+ assert parsed.month == 11
+ assert parsed.day == 12
+ assert parsed.hour == 20
+ assert parsed.minute == 12
+ assert parsed.second == 5
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "20:12:05.123456"
+
+ parsed = parse(text, now=now)
+
+ assert parsed.year == 2015
+ assert parsed.month == 11
+ assert parsed.day == 12
+ assert parsed.hour == 20
+ assert parsed.minute == 12
+ assert parsed.second == 5
+ assert parsed.microsecond == 123456
+ assert parsed.tzinfo is None
+
+
+def test_iso8601_ordinal_invalid():
+ text = "2012-007-05"
+
+ with pytest.raises(ParserError):
+ parse(text)
+
+
+def test_exact():
+ text = "2012"
+
+ parsed = parse(text, exact=True)
+
+ assert isinstance(parsed, datetime.date)
+ assert parsed.year == 2012
+ assert parsed.month == 1
+ assert parsed.day == 1
+
+ text = "2012-03"
+
+ parsed = parse(text, exact=True)
+
+ assert isinstance(parsed, datetime.date)
+ assert parsed.year == 2012
+ assert parsed.month == 3
+ assert parsed.day == 1
+
+ text = "2012-03-13"
+
+ parsed = parse(text, exact=True)
+
+ assert isinstance(parsed, datetime.date)
+ assert parsed.year == 2012
+ assert parsed.month == 3
+ assert parsed.day == 13
+
+ text = "2012W055"
+
+ parsed = parse(text, exact=True)
+
+ assert isinstance(parsed, datetime.date)
+ assert parsed.year == 2012
+ assert parsed.month == 2
+ assert parsed.day == 3
+
+ text = "2012007"
+
+ parsed = parse(text, exact=True)
+
+ assert isinstance(parsed, datetime.date)
+ assert parsed.year == 2012
+ assert parsed.month == 1
+ assert parsed.day == 7
+
+ text = "20:12:05"
+
+ parsed = parse(text, exact=True)
+
+ assert isinstance(parsed, datetime.time)
+ assert parsed.hour == 20
+ assert parsed.minute == 12
+ assert parsed.second == 5
+ assert parsed.microsecond == 0
+
+
+def test_edge_cases():
+ text = "2013-11-1"
+
+ parsed = parse(text, strict=False)
+ assert parsed.year == 2013
+ assert parsed.month == 11
+ assert parsed.day == 1
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "10-01-01"
+
+ parsed = parse(text, strict=False)
+ assert parsed.year == 2010
+ assert parsed.month == 1
+ assert parsed.day == 1
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "31-01-01"
+
+ parsed = parse(text, strict=False)
+ assert parsed.year == 2031
+ assert parsed.month == 1
+ assert parsed.day == 1
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+ text = "32-01-01"
+
+ parsed = parse(text, strict=False)
+ assert parsed.year == 2032
+ assert parsed.month == 1
+ assert parsed.day == 1
+ assert parsed.hour == 0
+ assert parsed.minute == 0
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+
+def test_strict():
+ text = "4 Aug 2015 - 11:20 PM"
+
+ with pytest.raises(ParserError):
+ parse(text)
+
+ parsed = parse(text, strict=False)
+ assert parsed.year == 2015
+ assert parsed.month == 8
+ assert parsed.day == 4
+ assert parsed.hour == 23
+ assert parsed.minute == 20
+ assert parsed.second == 0
+ assert parsed.microsecond == 0
+ assert parsed.tzinfo is None
+
+
+def test_invalid():
+ text = "201610T"
+
+ with pytest.raises(ParserError):
+ parse(text)
+
+ text = "2012-W54"
+
+ with pytest.raises(ParserError):
+ parse(text)
+
+ text = "2012-W13-8"
+
+ with pytest.raises(ParserError):
+ parse(text)
+
+ # W53 in normal year (not long)
+ text = "2017W53"
+
+ with pytest.raises(ParserError):
+ parse(text)
+
+
+def test_exif_edge_case():
+ text = "2016:12:26 15:45:28"
+
+ parsed = parse(text)
+
+ assert parsed.year == 2016
+ assert parsed.month == 12
+ assert parsed.day == 26
+ assert parsed.hour == 15
+ assert parsed.minute == 45
+ assert parsed.second == 28
diff --git a/tests/parsing/test_parsing_duration.py b/tests/parsing/test_parsing_duration.py
new file mode 100644
index 0000000..ab8b992
--- /dev/null
+++ b/tests/parsing/test_parsing_duration.py
@@ -0,0 +1,298 @@
+from __future__ import annotations
+
+import pytest
+
+from pendulum.parsing import ParserError
+from pendulum.parsing import parse
+
+
+def test_parse_duration():
+ text = "P2Y3M4DT5H6M7S"
+ parsed = parse(text)
+
+ assert parsed.years == 2
+ assert parsed.months == 3
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 4
+ assert parsed.hours == 5
+ assert parsed.minutes == 6
+ assert parsed.remaining_seconds == 7
+ assert parsed.microseconds == 0
+
+ text = "P1Y2M3DT4H5M6.5S"
+ parsed = parse(text)
+
+ assert parsed.years == 1
+ assert parsed.months == 2
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 3
+ assert parsed.hours == 4
+ assert parsed.minutes == 5
+ assert parsed.remaining_seconds == 6
+ assert parsed.microseconds == 500000
+
+ text = "P1Y2M3DT4H5M6,5S"
+ parsed = parse(text)
+
+ assert parsed.years == 1
+ assert parsed.months == 2
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 3
+ assert parsed.hours == 4
+ assert parsed.minutes == 5
+ assert parsed.remaining_seconds == 6
+ assert parsed.microseconds == 500000
+
+ text = "P1Y2M3D"
+ parsed = parse(text)
+
+ assert parsed.years == 1
+ assert parsed.months == 2
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 3
+ assert parsed.hours == 0
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1Y2M3.5D"
+ parsed = parse(text)
+
+ assert parsed.years == 1
+ assert parsed.months == 2
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 3
+ assert parsed.hours == 12
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1Y2M3,5D"
+ parsed = parse(text)
+
+ assert parsed.years == 1
+ assert parsed.months == 2
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 3
+ assert parsed.hours == 12
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "PT4H54M6.5S"
+ parsed = parse(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 0
+ assert parsed.hours == 4
+ assert parsed.minutes == 54
+ assert parsed.remaining_seconds == 6
+ assert parsed.microseconds == 500000
+
+ text = "PT4H54M6,5S"
+ parsed = parse(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 0
+ assert parsed.hours == 4
+ assert parsed.minutes == 54
+ assert parsed.remaining_seconds == 6
+ assert parsed.microseconds == 500000
+
+ text = "P1Y"
+ parsed = parse(text)
+
+ assert parsed.years == 1
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 0
+ assert parsed.hours == 0
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1.5Y"
+ with pytest.raises(ParserError):
+ parse(text)
+
+ text = "P1,5Y"
+ with pytest.raises(ParserError):
+ parse(text)
+
+ text = "P1M"
+ parsed = parse(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 1
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 0
+ assert parsed.hours == 0
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1.5M"
+ with pytest.raises(ParserError):
+ parse(text)
+
+ text = "P1,5M"
+ with pytest.raises(ParserError):
+ parse(text)
+
+ text = "P1W"
+ parsed = parse(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 1
+ assert parsed.remaining_days == 0
+ assert parsed.hours == 0
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1.5W"
+ parsed = parse(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 1
+ assert parsed.remaining_days == 3
+ assert parsed.hours == 12
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1,5W"
+ parsed = parse(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 1
+ assert parsed.remaining_days == 3
+ assert parsed.hours == 12
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1D"
+ parsed = parse(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 1
+ assert parsed.hours == 0
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1.5D"
+ parsed = parse(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 1
+ assert parsed.hours == 12
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "P1,5D"
+ parsed = parse(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 1
+ assert parsed.hours == 12
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "PT1H"
+ parsed = parse(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 0
+ assert parsed.hours == 1
+ assert parsed.minutes == 0
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "PT1.5H"
+ parsed = parse(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 0
+ assert parsed.hours == 1
+ assert parsed.minutes == 30
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+ text = "PT1,5H"
+ parsed = parse(text)
+
+ assert parsed.years == 0
+ assert parsed.months == 0
+ assert parsed.weeks == 0
+ assert parsed.remaining_days == 0
+ assert parsed.hours == 1
+ assert parsed.minutes == 30
+ assert parsed.remaining_seconds == 0
+ assert parsed.microseconds == 0
+
+
+def test_parse_duration_no_operator():
+ with pytest.raises(ParserError):
+ parse("2Y3M4DT5H6M7S")
+
+
+def test_parse_duration_weeks_combined():
+ with pytest.raises(ParserError):
+ parse("P1Y2W")
+
+
+def test_parse_duration_invalid_order():
+ with pytest.raises(ParserError):
+ parse("P1S")
+
+ with pytest.raises(ParserError):
+ parse("P1D1S")
+
+ with pytest.raises(ParserError):
+ parse("1Y2M3D1SPT1M")
+
+ with pytest.raises(ParserError):
+ parse("P1Y2M3D2MT1S")
+
+ with pytest.raises(ParserError):
+ parse("P2M3D1ST1Y1M")
+
+ with pytest.raises(ParserError):
+ parse("P1Y2M2MT3D1S")
+
+ with pytest.raises(ParserError):
+ parse("P1D1Y1M")
+
+ with pytest.raises(ParserError):
+ parse("PT1S1H")
+
+
+def test_parse_duration_invalid():
+ with pytest.raises(ParserError):
+ parse("P1Dasdfasdf")
+
+
+def test_parse_duration_fraction_only_allowed_on_last_component():
+ with pytest.raises(ParserError):
+ parse("P2Y3M4DT5.5H6M7S")
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
new file mode 100644
index 0000000..e3daeac
--- /dev/null
+++ b/tests/test_helpers.py
@@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from datetime import datetime
+
+import pytest
+import pytz
+
+import pendulum
+
+from pendulum import timezone
+from pendulum.helpers import days_in_year
+from pendulum.helpers import precise_diff
+from pendulum.helpers import week_day
+
+
+def assert_diff(
+ diff, years=0, months=0, days=0, hours=0, minutes=0, seconds=0, microseconds=0
+):
+ assert diff.years == years
+ assert diff.months == months
+ assert diff.days == days
+ assert diff.hours == hours
+ assert diff.minutes == minutes
+ assert diff.seconds == seconds
+ assert diff.microseconds == microseconds
+
+
+def test_precise_diff():
+ dt1 = datetime(2003, 3, 1, 0, 0, 0)
+ dt2 = datetime(2003, 1, 31, 23, 59, 59)
+
+ diff = precise_diff(dt1, dt2)
+ assert_diff(diff, months=-1, seconds=-1)
+
+ diff = precise_diff(dt2, dt1)
+ assert_diff(diff, months=1, seconds=1)
+
+ dt1 = datetime(2012, 3, 1, 0, 0, 0)
+ dt2 = datetime(2012, 1, 31, 23, 59, 59)
+
+ diff = precise_diff(dt1, dt2)
+ assert_diff(diff, months=-1, seconds=-1)
+
+ diff = precise_diff(dt2, dt1)
+ assert_diff(diff, months=1, seconds=1)
+
+ dt1 = datetime(2001, 1, 1)
+ dt2 = datetime(2003, 9, 17, 20, 54, 47, 282310)
+
+ diff = precise_diff(dt1, dt2)
+ assert_diff(
+ diff,
+ years=2,
+ months=8,
+ days=16,
+ hours=20,
+ minutes=54,
+ seconds=47,
+ microseconds=282310,
+ )
+
+ dt1 = datetime(2017, 2, 17, 16, 5, 45, 123456)
+ dt2 = datetime(2018, 2, 17, 16, 5, 45, 123256)
+
+ diff = precise_diff(dt1, dt2)
+ assert_diff(
+ diff, months=11, days=30, hours=23, minutes=59, seconds=59, microseconds=999800
+ )
+
+ # DST
+ tz = timezone("America/Toronto")
+ dt1 = tz.datetime(2017, 3, 7)
+ dt2 = tz.datetime(2017, 3, 13)
+
+ diff = precise_diff(dt1, dt2)
+ assert_diff(diff, days=6, hours=0)
+
+
+def test_precise_diff_timezone():
+ paris = pendulum.timezone("Europe/Paris")
+ toronto = pendulum.timezone("America/Toronto")
+
+ dt1 = paris.datetime(2013, 3, 31, 1, 30)
+ dt2 = paris.datetime(2013, 4, 1, 1, 30)
+
+ diff = precise_diff(dt1, dt2)
+ assert_diff(diff, days=1, hours=0)
+
+ dt2 = toronto.datetime(2013, 4, 1, 1, 30)
+
+ diff = precise_diff(dt1, dt2)
+ assert_diff(diff, days=1, hours=5)
+
+ # pytz
+ paris = pytz.timezone("Europe/Paris")
+ toronto = pytz.timezone("America/Toronto")
+
+ dt1 = paris.localize(datetime(2013, 3, 31, 1, 30))
+ dt2 = paris.localize(datetime(2013, 4, 1, 1, 30))
+
+ diff = precise_diff(dt1, dt2)
+ assert_diff(diff, days=1, hours=0)
+
+ dt2 = toronto.localize(datetime(2013, 4, 1, 1, 30))
+
+ diff = precise_diff(dt1, dt2)
+ assert_diff(diff, days=1, hours=5)
+
+ # Issue238
+ dt1 = timezone("UTC").datetime(2018, 6, 20, 1, 30)
+ dt2 = timezone("Europe/Paris").datetime(2018, 6, 20, 3, 40) # UTC+2
+ diff = precise_diff(dt1, dt2)
+ assert_diff(diff, minutes=10)
+
+
+def test_week_day():
+ assert week_day(2017, 6, 2) == 5
+ assert week_day(2017, 1, 1) == 7
+
+
+def test_days_in_years():
+ assert days_in_year(2017) == 365
+ assert days_in_year(2016) == 366
+
+
+def test_locale():
+ dt = pendulum.datetime(2000, 11, 10, 12, 34, 56, 123456)
+ pendulum.set_locale("fr")
+
+ assert pendulum.get_locale() == "fr"
+
+ assert dt.format("MMMM") == "novembre"
+ assert dt.date().format("MMMM") == "novembre"
+
+
+def test_set_locale_invalid():
+ with pytest.raises(ValueError):
+ pendulum.set_locale("invalid")
+
+
+@pytest.mark.parametrize(
+ "locale", ["DE", "pt-BR", "pt-br", "PT-br", "PT-BR", "pt_br", "PT_BR", "PT_BR"]
+)
+def test_set_locale_malformed_locale(locale):
+ pendulum.set_locale(locale)
+
+ pendulum.set_locale("en")
+
+
+def test_week_starts_at():
+ pendulum.week_starts_at(pendulum.SATURDAY)
+
+ dt = pendulum.now().start_of("week")
+ assert dt.day_of_week == pendulum.SATURDAY
+ assert dt.date().day_of_week == pendulum.SATURDAY
+
+
+def test_week_starts_at_invalid_value():
+ with pytest.raises(ValueError):
+ pendulum.week_starts_at(-1)
+
+ with pytest.raises(ValueError):
+ pendulum.week_starts_at(11)
+
+
+def test_week_ends_at():
+ pendulum.week_ends_at(pendulum.SATURDAY)
+
+ dt = pendulum.now().end_of("week")
+ assert dt.day_of_week == pendulum.SATURDAY
+ assert dt.date().day_of_week == pendulum.SATURDAY
+
+
+def test_week_ends_at_invalid_value():
+ with pytest.raises(ValueError):
+ pendulum.week_ends_at(-1)
+
+ with pytest.raises(ValueError):
+ pendulum.week_ends_at(11)
diff --git a/tests/test_main.py b/tests/test_main.py
new file mode 100644
index 0000000..1710bf2
--- /dev/null
+++ b/tests/test_main.py
@@ -0,0 +1,13 @@
+from __future__ import annotations
+
+import pytz
+
+from pendulum import _safe_timezone
+from pendulum.tz.timezone import Timezone
+
+
+def test_safe_timezone_with_tzinfo_objects():
+ tz = _safe_timezone(pytz.timezone("Europe/Paris"))
+
+ assert isinstance(tz, Timezone)
+ assert tz.name == "Europe/Paris"
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
new file mode 100644
index 0000000..0e5308c
--- /dev/null
+++ b/tests/test_parsing.py
@@ -0,0 +1,141 @@
+from __future__ import annotations
+
+import pendulum
+
+from tests.conftest import assert_date
+from tests.conftest import assert_datetime
+from tests.conftest import assert_duration
+from tests.conftest import assert_time
+
+
+def test_parse():
+ text = "2016-10-16T12:34:56.123456+01:30"
+
+ dt = pendulum.parse(text)
+
+ assert isinstance(dt, pendulum.DateTime)
+ assert_datetime(dt, 2016, 10, 16, 12, 34, 56, 123456)
+ assert dt.tz.name == "+01:30"
+ assert dt.offset == 5400
+
+ text = "2016-10-16"
+
+ dt = pendulum.parse(text)
+
+ assert isinstance(dt, pendulum.DateTime)
+ assert_datetime(dt, 2016, 10, 16, 0, 0, 0, 0)
+ assert dt.offset == 0
+
+ with pendulum.travel_to(pendulum.datetime(2015, 11, 12), freeze=True):
+ text = "12:34:56.123456"
+
+ dt = pendulum.parse(text)
+
+ assert isinstance(dt, pendulum.DateTime)
+ assert_datetime(dt, 2015, 11, 12, 12, 34, 56, 123456)
+ assert dt.offset == 0
+
+
+def test_parse_with_timezone():
+ text = "2016-10-16T12:34:56.123456"
+
+ dt = pendulum.parse(text, tz="Europe/Paris")
+ assert_datetime(dt, 2016, 10, 16, 12, 34, 56, 123456)
+ assert dt.tz.name == "Europe/Paris"
+ assert dt.offset == 7200
+
+
+def test_parse_exact():
+ text = "2016-10-16T12:34:56.123456+01:30"
+
+ dt = pendulum.parse(text, exact=True)
+
+ assert isinstance(dt, pendulum.DateTime)
+ assert_datetime(dt, 2016, 10, 16, 12, 34, 56, 123456)
+ assert dt.offset == 5400
+
+ text = "2016-10-16"
+
+ dt = pendulum.parse(text, exact=True)
+
+ assert isinstance(dt, pendulum.Date)
+ assert_date(dt, 2016, 10, 16)
+
+ text = "12:34:56.123456"
+
+ dt = pendulum.parse(text, exact=True)
+
+ assert isinstance(dt, pendulum.Time)
+ assert_time(dt, 12, 34, 56, 123456)
+
+ text = "13:00"
+
+ dt = pendulum.parse(text, exact=True)
+
+ assert isinstance(dt, pendulum.Time)
+ assert_time(dt, 13, 0, 0)
+
+
+def test_parse_duration():
+ text = "P2Y3M4DT5H6M7S"
+
+ duration = pendulum.parse(text)
+
+ assert isinstance(duration, pendulum.Duration)
+ assert_duration(duration, 2, 3, 0, 4, 5, 6, 7)
+
+ text = "P2W"
+
+ duration = pendulum.parse(text)
+
+ assert isinstance(duration, pendulum.Duration)
+ assert_duration(duration, 0, 0, 2, 0, 0, 0, 0)
+
+
+def test_parse_interval():
+ text = "2008-05-11T15:30:00Z/P1Y2M10DT2H30M"
+
+ period = pendulum.parse(text)
+
+ assert isinstance(period, pendulum.Interval)
+ assert_datetime(period.start, 2008, 5, 11, 15, 30, 0, 0)
+ assert period.start.offset == 0
+ assert_datetime(period.end, 2009, 7, 21, 18, 0, 0, 0)
+ assert period.end.offset == 0
+
+ text = "P1Y2M10DT2H30M/2008-05-11T15:30:00Z"
+
+ period = pendulum.parse(text)
+
+ assert isinstance(period, pendulum.Interval)
+ assert_datetime(period.start, 2007, 3, 1, 13, 0, 0, 0)
+ assert period.start.offset == 0
+ assert_datetime(period.end, 2008, 5, 11, 15, 30, 0, 0)
+ assert period.end.offset == 0
+
+ text = "2007-03-01T13:00:00Z/2008-05-11T15:30:00Z"
+
+ period = pendulum.parse(text)
+
+ assert isinstance(period, pendulum.Interval)
+ assert_datetime(period.start, 2007, 3, 1, 13, 0, 0, 0)
+ assert period.start.offset == 0
+ assert_datetime(period.end, 2008, 5, 11, 15, 30, 0, 0)
+ assert period.end.offset == 0
+
+
+def test_parse_now():
+ dt = pendulum.parse("now")
+
+ assert dt.timezone_name == "America/Toronto"
+
+ mock_now = pendulum.yesterday()
+
+ with pendulum.travel_to(mock_now, freeze=True):
+ assert pendulum.parse("now") == mock_now
+
+
+def test_parse_with_utc_timezone():
+ dt = pendulum.parse("2020-02-05T20:05:37.364951Z")
+
+ assert dt.to_iso8601_string() == "2020-02-05T20:05:37.364951Z"
diff --git a/tests/testing/__init__.py b/tests/testing/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/testing/__init__.py
diff --git a/tests/testing/test_time_travel.py b/tests/testing/test_time_travel.py
new file mode 100644
index 0000000..dd496e3
--- /dev/null
+++ b/tests/testing/test_time_travel.py
@@ -0,0 +1,85 @@
+from __future__ import annotations
+
+from time import sleep
+from typing import TYPE_CHECKING
+
+import pytest
+
+import pendulum
+
+from pendulum.utils._compat import PYPY
+
+if TYPE_CHECKING:
+ from typing import Generator
+
+
+@pytest.fixture(autouse=True)
+def setup() -> Generator[None, None, None]:
+ pendulum.travel_back()
+
+ yield
+
+ pendulum.travel_back()
+
+
+@pytest.mark.skipif(PYPY, reason="Time travelling not available on PyPy")
+def test_travel() -> None:
+ now = pendulum.now()
+
+ pendulum.travel(minutes=5)
+
+ assert pendulum.now().diff_for_humans(now) == "5 minutes after"
+
+
+@pytest.mark.skipif(PYPY, reason="Time travelling not available on PyPy")
+def test_travel_with_frozen_time() -> None:
+ pendulum.travel(minutes=5, freeze=True)
+
+ now = pendulum.now()
+
+ sleep(0.01)
+
+ assert now == pendulum.now()
+
+
+@pytest.mark.skipif(PYPY, reason="Time travelling not available on PyPy")
+def test_travel_to() -> None:
+ dt = pendulum.datetime(2022, 1, 19, tz="local")
+
+ pendulum.travel_to(dt)
+
+ assert pendulum.now().date() == dt.date()
+
+
+@pytest.mark.skipif(PYPY, reason="Time travelling not available on PyPy")
+def test_freeze() -> None:
+ pendulum.freeze()
+
+ pendulum.travel(minutes=5)
+
+ assert pendulum.now() == pendulum.now()
+
+ pendulum.travel_back()
+
+ pendulum.travel(minutes=5)
+
+ now = pendulum.now()
+
+ sleep(0.01)
+
+ assert now != pendulum.now()
+
+ pendulum.freeze()
+
+ assert pendulum.now() == pendulum.now()
+
+ pendulum.travel_back()
+
+ with pendulum.freeze():
+ assert pendulum.now() == pendulum.now()
+
+ now = pendulum.now()
+
+ sleep(0.01)
+
+ assert now != pendulum.now()
diff --git a/tests/time/__init__.py b/tests/time/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/time/__init__.py
diff --git a/tests/time/test_add.py b/tests/time/test_add.py
new file mode 100644
index 0000000..7075ebe
--- /dev/null
+++ b/tests/time/test_add.py
@@ -0,0 +1,78 @@
+from __future__ import annotations
+
+from datetime import timedelta
+
+import pytest
+
+import pendulum
+
+
+def test_add_hours_positive():
+ assert pendulum.time(12, 34, 56).add(hours=1).hour == 13
+
+
+def test_add_hours_zero():
+ assert pendulum.time(12, 34, 56).add(hours=0).hour == 12
+
+
+def test_add_hours_negative():
+ assert pendulum.time(12, 34, 56).add(hours=-1).hour == 11
+
+
+def test_add_minutes_positive():
+ assert pendulum.time(12, 34, 56).add(minutes=1).minute == 35
+
+
+def test_add_minutes_zero():
+ assert pendulum.time(12, 34, 56).add(minutes=0).minute == 34
+
+
+def test_add_minutes_negative():
+ assert pendulum.time(12, 34, 56).add(minutes=-1).minute == 33
+
+
+def test_add_seconds_positive():
+ assert pendulum.time(12, 34, 56).add(seconds=1).second == 57
+
+
+def test_add_seconds_zero():
+ assert pendulum.time(12, 34, 56).add(seconds=0).second == 56
+
+
+def test_add_seconds_negative():
+ assert pendulum.time(12, 34, 56).add(seconds=-1).second == 55
+
+
+def test_add_timedelta():
+ delta = timedelta(seconds=45, microseconds=123456)
+ d = pendulum.time(3, 12, 15, 654321)
+
+ d = d.add_timedelta(delta)
+ assert d.minute == 13
+ assert d.second == 0
+ assert d.microsecond == 777777
+
+ d = pendulum.time(3, 12, 15, 654321)
+
+ d = d + delta
+ assert d.minute == 13
+ assert d.second == 0
+ assert d.microsecond == 777777
+
+
+def test_add_timedelta_with_days():
+ delta = timedelta(days=3, seconds=45, microseconds=123456)
+ d = pendulum.time(3, 12, 15, 654321)
+
+ with pytest.raises(TypeError):
+ d.add_timedelta(delta)
+
+
+def test_addition_invalid_type():
+ d = pendulum.time(3, 12, 15, 654321)
+
+ with pytest.raises(TypeError):
+ d + 3
+
+ with pytest.raises(TypeError):
+ 3 + d
diff --git a/tests/time/test_behavior.py b/tests/time/test_behavior.py
new file mode 100644
index 0000000..0071c94
--- /dev/null
+++ b/tests/time/test_behavior.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+import pickle
+
+from datetime import time
+
+import pytest
+
+import pendulum
+
+from pendulum import Time
+
+
+@pytest.fixture()
+def p():
+ return pendulum.Time(12, 34, 56, 123456, tzinfo=pendulum.timezone("Europe/Paris"))
+
+
+@pytest.fixture()
+def d():
+ return time(12, 34, 56, 123456, tzinfo=pendulum.timezone("Europe/Paris"))
+
+
+def test_hash(p, d):
+ assert hash(d) == hash(p)
+ dt1 = Time(12, 34, 57, 123456)
+
+ assert hash(p) != hash(dt1)
+
+
+def test_pickle():
+ dt1 = Time(12, 34, 56, 123456)
+ s = pickle.dumps(dt1)
+ dt2 = pickle.loads(s)
+
+ assert dt2 == dt1
+
+
+def test_utcoffset(p, d):
+ assert d.utcoffset() == p.utcoffset()
+
+
+def test_dst(p, d):
+ assert d.dst() == p.dst()
+
+
+def test_tzname(p, d):
+ assert d.tzname() == p.tzname()
+ assert Time(12, 34, 56, 123456).tzname() == time(12, 34, 56, 123456).tzname()
diff --git a/tests/time/test_comparison.py b/tests/time/test_comparison.py
new file mode 100644
index 0000000..f1ef275
--- /dev/null
+++ b/tests/time/test_comparison.py
@@ -0,0 +1,185 @@
+from __future__ import annotations
+
+from datetime import time
+
+import pendulum
+
+from tests.conftest import assert_time
+
+
+def test_equal_to_true():
+ t1 = pendulum.time(1, 2, 3)
+ t2 = pendulum.time(1, 2, 3)
+ t3 = time(1, 2, 3)
+
+ assert t1 == t2
+ assert t1 == t3
+
+
+def test_equal_to_false():
+ t1 = pendulum.time(1, 2, 3)
+ t2 = pendulum.time(1, 2, 4)
+ t3 = time(1, 2, 4)
+
+ assert t1 != t2
+ assert t1 != t3
+
+
+def test_not_equal_to_none():
+ t1 = pendulum.time(1, 2, 3)
+
+ assert t1 != None # noqa
+
+
+def test_greater_than_true():
+ t1 = pendulum.time(1, 2, 3)
+ t2 = pendulum.time(1, 2, 2)
+ t3 = time(1, 2, 2)
+
+ assert t1 > t2
+ assert t1 > t3
+
+
+def test_greater_than_false():
+ t1 = pendulum.time(1, 2, 2)
+ t2 = pendulum.time(1, 2, 3)
+ t3 = time(1, 2, 3)
+
+ assert not t1 > t2
+ assert not t1 > t3
+
+
+def test_greater_than_or_equal_true():
+ t1 = pendulum.time(1, 2, 3)
+ t2 = pendulum.time(1, 2, 2)
+ t3 = time(1, 2, 2)
+
+ assert t1 >= t2
+ assert t1 >= t3
+
+
+def test_greater_than_or_equal_true_equal():
+ t1 = pendulum.time(1, 2, 3)
+ t2 = pendulum.time(1, 2, 3)
+ t3 = time(1, 2, 3)
+
+ assert t1 >= t2
+ assert t1 >= t3
+
+
+def test_greater_than_or_equal_false():
+ t1 = pendulum.time(1, 2, 2)
+ t2 = pendulum.time(1, 2, 3)
+ t3 = time(1, 2, 3)
+
+ assert not t1 >= t2
+ assert not t1 >= t3
+
+
+def test_less_than_true():
+ t1 = pendulum.time(1, 2, 2)
+ t2 = pendulum.time(1, 2, 3)
+ t3 = time(1, 2, 3)
+
+ assert t1 < t2
+ assert t1 < t3
+
+
+def test_less_than_false():
+ t1 = pendulum.time(1, 2, 3)
+ t2 = pendulum.time(1, 2, 2)
+ t3 = time(1, 2, 2)
+
+ assert not t1 < t2
+ assert not t1 < t3
+
+
+def test_less_than_or_equal_true():
+ t1 = pendulum.time(1, 2, 2)
+ t2 = pendulum.time(1, 2, 3)
+ t3 = time(1, 2, 3)
+
+ assert t1 <= t2
+ assert t1 <= t3
+
+
+def test_less_than_or_equal_true_equal():
+ t1 = pendulum.time(1, 2, 3)
+ t2 = pendulum.time(1, 2, 3)
+ t3 = time(1, 2, 3)
+
+ assert t1 <= t2
+ assert t1 <= t3
+
+
+def test_less_than_or_equal_false():
+ t1 = pendulum.time(1, 2, 3)
+ t2 = pendulum.time(1, 2, 2)
+ t3 = time(1, 2, 2)
+
+ assert not t1 <= t2
+ assert not t1 <= t3
+
+
+def test_closest():
+ instance = pendulum.time(12, 34, 56)
+ t1 = pendulum.time(12, 34, 54)
+ t2 = pendulum.time(12, 34, 59)
+ closest = instance.closest(t1, t2)
+ assert t1 == closest
+
+ closest = instance.closest(t2, t1)
+ assert t1 == closest
+
+
+def test_closest_with_time():
+ instance = pendulum.time(12, 34, 56)
+ t1 = pendulum.time(12, 34, 54)
+ t2 = pendulum.time(12, 34, 59)
+ closest = instance.closest(t1, t2)
+
+ assert_time(closest, 12, 34, 54)
+
+
+def test_closest_with_equals():
+ instance = pendulum.time(12, 34, 56)
+ t1 = pendulum.time(12, 34, 56)
+ t2 = pendulum.time(12, 34, 59)
+ closest = instance.closest(t1, t2)
+ assert t1 == closest
+
+
+def test_farthest():
+ instance = pendulum.time(12, 34, 56)
+ t1 = pendulum.time(12, 34, 54)
+ t2 = pendulum.time(12, 34, 59)
+ farthest = instance.farthest(t1, t2)
+ assert t2 == farthest
+
+ farthest = instance.farthest(t2, t1)
+ assert t2 == farthest
+
+
+def test_farthest_with_time():
+ instance = pendulum.time(12, 34, 56)
+ t1 = pendulum.time(12, 34, 54)
+ t2 = pendulum.time(12, 34, 59)
+ farthest = instance.farthest(t1, t2)
+
+ assert_time(farthest, 12, 34, 59)
+
+
+def test_farthest_with_equals():
+ instance = pendulum.time(12, 34, 56)
+ t1 = pendulum.time(12, 34, 56)
+ t2 = pendulum.time(12, 34, 59)
+
+ farthest = instance.farthest(t1, t2)
+ assert t2 == farthest
+
+
+def test_comparison_to_unsupported():
+ t1 = pendulum.now().time()
+
+ assert t1 != "test"
+ assert t1 not in ["test"]
diff --git a/tests/time/test_construct.py b/tests/time/test_construct.py
new file mode 100644
index 0000000..7d81b37
--- /dev/null
+++ b/tests/time/test_construct.py
@@ -0,0 +1,22 @@
+from __future__ import annotations
+
+import pendulum
+
+from tests.conftest import assert_time
+
+
+def test_init():
+ t = pendulum.time(12, 34, 56, 123456)
+
+ assert_time(t, 12, 34, 56, 123456)
+
+
+def test_init_with_missing_values():
+ t = pendulum.time(12, 34, 56)
+ assert_time(t, 12, 34, 56, 0)
+
+ t = pendulum.time(12, 34)
+ assert_time(t, 12, 34, 0, 0)
+
+ t = pendulum.time(12)
+ assert_time(t, 12, 0, 0, 0)
diff --git a/tests/time/test_diff.py b/tests/time/test_diff.py
new file mode 100644
index 0000000..629a058
--- /dev/null
+++ b/tests/time/test_diff.py
@@ -0,0 +1,350 @@
+from __future__ import annotations
+
+import pendulum
+
+from pendulum import Time
+
+
+def test_diff_in_hours_positive():
+ dt = Time(12, 34, 56)
+ assert dt.diff(dt.add(hours=2).add(seconds=3672)).in_hours() == 3
+
+
+def test_diff_in_hours_negative_with_sign():
+ dt = Time(12, 34, 56)
+ assert dt.diff(dt.subtract(hours=2).add(seconds=3600), False).in_hours() == -1
+
+
+def test_diff_in_hours_negative_no_sign():
+ dt = Time(12, 34, 56)
+ assert dt.diff(dt.subtract(hours=2).add(seconds=3600)).in_hours() == 1
+
+
+def test_diff_in_hours_vs_default_now():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.subtract(hours=2).diff().in_hours() == 2
+
+
+def test_diff_in_hours_ensure_is_truncated():
+ dt = Time(12, 34, 56)
+ assert dt.diff(dt.add(hours=2).add(seconds=5401)).in_hours() == 3
+
+
+def test_diff_in_minutes_positive():
+ dt = Time(12, 34, 56)
+ assert dt.diff(dt.add(hours=1).add(minutes=2)).in_minutes() == 62
+
+
+def test_diff_in_minutes_positive_big():
+ dt = Time(12, 34, 56)
+ assert dt.diff(dt.add(hours=25).add(minutes=2)).in_minutes() == 62
+
+
+def test_diff_in_minutes_negative_with_sign():
+ dt = Time(12, 34, 56)
+ assert dt.diff(dt.subtract(hours=1).add(minutes=2), False).in_minutes() == -58
+
+
+def test_diff_in_minutes_negative_no_sign():
+ dt = Time(12, 34, 56)
+ assert dt.diff(dt.subtract(hours=1).add(minutes=2)).in_minutes() == 58
+
+
+def test_diff_in_minutes_vs_default_now():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.subtract(hours=1).diff().in_minutes() == 60
+
+
+def test_diff_in_minutes_ensure_is_truncated():
+ dt = Time(12, 34, 56)
+ assert dt.diff(dt.add(minutes=1).add(seconds=59)).in_minutes() == 1
+
+
+def test_diff_in_seconds_positive():
+ dt = Time(12, 34, 56)
+ assert dt.diff(dt.add(minutes=1).add(seconds=2)).in_seconds() == 62
+
+
+def test_diff_in_seconds_positive_big():
+ dt = Time(12, 34, 56)
+ assert dt.diff(dt.add(hours=2).add(seconds=2)).in_seconds() == 7202
+
+
+def test_diff_in_seconds_negative_with_sign():
+ dt = Time(12, 34, 56)
+ assert dt.diff(dt.subtract(minutes=1).add(seconds=2), False).in_seconds() == -58
+
+
+def test_diff_in_seconds_negative_no_sign():
+ dt = Time(12, 34, 56)
+ assert dt.diff(dt.subtract(minutes=1).add(seconds=2)).in_seconds() == 58
+
+
+def test_diff_in_seconds_vs_default_now():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.subtract(hours=1).diff().in_seconds() == 3600
+
+
+def test_diff_in_seconds_ensure_is_truncated():
+ dt = Time(12, 34, 56)
+ assert dt.diff(dt.add(seconds=1.9)).in_seconds() == 1
+
+
+def test_diff_for_humans_now_and_second():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans() == "a few seconds ago"
+
+
+def test_diff_for_humans_now_and_seconds():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.subtract(seconds=2).diff_for_humans() == "a few seconds ago"
+
+
+def test_diff_for_humans_now_and_nearly_minute():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.subtract(seconds=59).diff_for_humans() == "59 seconds ago"
+
+
+def test_diff_for_humans_now_and_minute():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.subtract(minutes=1).diff_for_humans() == "1 minute ago"
+
+
+def test_diff_for_humans_now_and_minutes():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.subtract(minutes=2).diff_for_humans() == "2 minutes ago"
+
+
+def test_diff_for_humans_now_and_nearly_hour():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.subtract(minutes=59).diff_for_humans() == "59 minutes ago"
+
+
+def test_diff_for_humans_now_and_hour():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.subtract(hours=1).diff_for_humans() == "1 hour ago"
+
+
+def test_diff_for_humans_now_and_hours():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.subtract(hours=2).diff_for_humans() == "2 hours ago"
+
+
+def test_diff_for_humans_now_and_future_second():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.add(seconds=1).diff_for_humans() == "in a few seconds"
+
+
+def test_diff_for_humans_now_and_future_seconds():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.add(seconds=2).diff_for_humans() == "in a few seconds"
+
+
+def test_diff_for_humans_now_and_nearly_future_minute():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.add(seconds=59).diff_for_humans() == "in 59 seconds"
+
+
+def test_diff_for_humans_now_and_future_minute():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.add(minutes=1).diff_for_humans() == "in 1 minute"
+
+
+def test_diff_for_humans_now_and_future_minutes():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.add(minutes=2).diff_for_humans() == "in 2 minutes"
+
+
+def test_diff_for_humans_now_and_nearly_future_hour():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.add(minutes=59).diff_for_humans() == "in 59 minutes"
+
+
+def test_diff_for_humans_now_and_future_hour():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.add(hours=1).diff_for_humans() == "in 1 hour"
+
+
+def test_diff_for_humans_now_and_future_hours():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.add(hours=2).diff_for_humans() == "in 2 hours"
+
+
+def test_diff_for_humans_other_and_second():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.add(seconds=1)) == "a few seconds before"
+
+
+def test_diff_for_humans_other_and_seconds():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.add(seconds=2)) == "a few seconds before"
+
+
+def test_diff_for_humans_other_and_nearly_minute():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.add(seconds=59)) == "59 seconds before"
+
+
+def test_diff_for_humans_other_and_minute():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.add(minutes=1)) == "1 minute before"
+
+
+def test_diff_for_humans_other_and_minutes():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.add(minutes=2)) == "2 minutes before"
+
+
+def test_diff_for_humans_other_and_nearly_hour():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.add(minutes=59)) == "59 minutes before"
+
+
+def test_diff_for_humans_other_and_hour():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.add(hours=1)) == "1 hour before"
+
+
+def test_diff_for_humans_other_and_hours():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.add(hours=2)) == "2 hours before"
+
+
+def test_diff_for_humans_other_and_future_second():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.subtract(seconds=1)) == "a few seconds after"
+
+
+def test_diff_for_humans_other_and_future_seconds():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.subtract(seconds=2)) == "a few seconds after"
+
+
+def test_diff_for_humans_other_and_nearly_future_minute():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.subtract(seconds=59)) == "59 seconds after"
+
+
+def test_diff_for_humans_other_and_future_minute():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.subtract(minutes=1)) == "1 minute after"
+
+
+def test_diff_for_humans_other_and_future_minutes():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.subtract(minutes=2)) == "2 minutes after"
+
+
+def test_diff_for_humans_other_and_nearly_future_hour():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.subtract(minutes=59)) == "59 minutes after"
+
+
+def test_diff_for_humans_other_and_future_hour():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.subtract(hours=1)) == "1 hour after"
+
+
+def test_diff_for_humans_other_and_future_hours():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.subtract(hours=2)) == "2 hours after"
+
+
+def test_diff_for_humans_absolute_seconds():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.subtract(seconds=59), True) == "59 seconds"
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.add(seconds=59), True) == "59 seconds"
+
+
+def test_diff_for_humans_absolute_minutes():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.subtract(minutes=30), True) == "30 minutes"
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.add(minutes=30), True) == "30 minutes"
+
+
+def test_diff_for_humans_absolute_hours():
+ with pendulum.travel_to(pendulum.today().at(12, 34, 56)):
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.subtract(hours=3), True) == "3 hours"
+ now = pendulum.now().time()
+
+ assert now.diff_for_humans(now.add(hours=3), True) == "3 hours"
diff --git a/tests/time/test_fluent_setters.py b/tests/time/test_fluent_setters.py
new file mode 100644
index 0000000..a678e56
--- /dev/null
+++ b/tests/time/test_fluent_setters.py
@@ -0,0 +1,12 @@
+from __future__ import annotations
+
+from pendulum import Time
+from tests.conftest import assert_time
+
+
+def test_replace():
+ t = Time(12, 34, 56, 123456)
+ t = t.replace(1, 2, 3, 654321)
+
+ assert isinstance(t, Time)
+ assert_time(t, 1, 2, 3, 654321)
diff --git a/tests/time/test_strings.py b/tests/time/test_strings.py
new file mode 100644
index 0000000..db3c9cd
--- /dev/null
+++ b/tests/time/test_strings.py
@@ -0,0 +1,39 @@
+from __future__ import annotations
+
+from pendulum import Time
+
+
+def test_to_string():
+ d = Time(1, 2, 3)
+ assert str(d) == "01:02:03"
+ d = Time(1, 2, 3, 123456)
+ assert str(d) == "01:02:03.123456"
+
+
+def test_repr():
+ d = Time(1, 2, 3)
+ assert repr(d) == "Time(1, 2, 3)"
+
+ d = Time(1, 2, 3, 123456)
+ assert repr(d) == "Time(1, 2, 3, 123456)"
+
+
+def test_format_with_locale():
+ d = Time(14, 15, 16)
+ assert d.format("hh:mm:ss A", locale="fr") == "02:15:16 PM"
+
+
+def test_strftime():
+ d = Time(14, 15, 16)
+ assert d.strftime("%H") == "14"
+
+
+def test_for_json():
+ d = Time(14, 15, 16)
+ assert d.for_json() == "14:15:16"
+
+
+def test_format():
+ d = Time(14, 15, 16)
+ assert f"{d}" == "14:15:16"
+ assert f"{d:mm}" == "15"
diff --git a/tests/time/test_sub.py b/tests/time/test_sub.py
new file mode 100644
index 0000000..1a957ad
--- /dev/null
+++ b/tests/time/test_sub.py
@@ -0,0 +1,112 @@
+from __future__ import annotations
+
+from datetime import time
+from datetime import timedelta
+
+import pytest
+import pytz
+
+import pendulum
+
+from pendulum import Time
+from tests.conftest import assert_duration
+
+
+def test_sub_hours_positive():
+ assert Time(0, 0, 0).subtract(hours=1).hour == 23
+
+
+def test_sub_hours_zero():
+ assert Time(0, 0, 0).subtract(hours=0).hour == 0
+
+
+def test_sub_hours_negative():
+ assert Time(0, 0, 0).subtract(hours=-1).hour == 1
+
+
+def test_sub_minutes_positive():
+ assert Time(0, 0, 0).subtract(minutes=1).minute == 59
+
+
+def test_sub_minutes_zero():
+ assert Time(0, 0, 0).subtract(minutes=0).minute == 0
+
+
+def test_sub_minutes_negative():
+ assert Time(0, 0, 0).subtract(minutes=-1).minute == 1
+
+
+def test_sub_seconds_positive():
+ assert Time(0, 0, 0).subtract(seconds=1).second == 59
+
+
+def test_sub_seconds_zero():
+ assert Time(0, 0, 0).subtract(seconds=0).second == 0
+
+
+def test_sub_seconds_negative():
+ assert Time(0, 0, 0).subtract(seconds=-1).second == 1
+
+
+def test_subtract_timedelta():
+ delta = timedelta(seconds=16, microseconds=654321)
+ d = Time(3, 12, 15, 777777)
+
+ d = d.subtract_timedelta(delta)
+ assert d.minute == 11
+ assert d.second == 59
+ assert d.microsecond == 123456
+
+ d = Time(3, 12, 15, 777777)
+
+ d = d - delta
+ assert d.minute == 11
+ assert d.second == 59
+ assert d.microsecond == 123456
+
+
+def test_add_timedelta_with_days():
+ delta = timedelta(days=3, seconds=45, microseconds=123456)
+ d = Time(3, 12, 15, 654321)
+
+ with pytest.raises(TypeError):
+ d.subtract_timedelta(delta)
+
+
+def test_subtract_invalid_type():
+ d = Time(0, 0, 0)
+
+ with pytest.raises(TypeError):
+ d - "ab"
+
+ with pytest.raises(TypeError):
+ "ab" - d
+
+
+def test_subtract_time():
+ t = Time(12, 34, 56)
+ t1 = Time(1, 1, 1)
+ t2 = time(1, 1, 1)
+ t3 = time(1, 1, 1, tzinfo=pytz.timezone("Europe/Paris"))
+
+ diff = t - t1
+ assert isinstance(diff, pendulum.Duration)
+ assert_duration(diff, 0, hours=11, minutes=33, seconds=55)
+
+ diff = t1 - t
+ assert isinstance(diff, pendulum.Duration)
+ assert_duration(diff, 0, hours=-11, minutes=-33, seconds=-55)
+
+ diff = t - t2
+ assert isinstance(diff, pendulum.Duration)
+ assert_duration(diff, 0, hours=11, minutes=33, seconds=55)
+
+ diff = t2 - t
+ assert isinstance(diff, pendulum.Duration)
+ assert_duration(diff, 0, hours=-11, minutes=-33, seconds=-55)
+
+ with pytest.raises(TypeError):
+ t - t3
+
+ with pytest.raises(TypeError):
+ t3 - t
diff --git a/tests/tz/__init__.py b/tests/tz/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/tz/__init__.py
diff --git a/tests/tz/test_helpers.py b/tests/tz/test_helpers.py
new file mode 100644
index 0000000..edec6fd
--- /dev/null
+++ b/tests/tz/test_helpers.py
@@ -0,0 +1,27 @@
+from __future__ import annotations
+
+import pytest
+
+from pendulum.tz import timezone
+from pendulum.tz.exceptions import InvalidTimezone
+from pendulum.tz.timezone import FixedTimezone
+from pendulum.tz.timezone import Timezone
+
+
+def test_timezone_with_name():
+ tz = timezone("Europe/Paris")
+
+ assert isinstance(tz, Timezone)
+ assert tz.name == "Europe/Paris"
+
+
+def test_timezone_with_invalid_name():
+ with pytest.raises(InvalidTimezone):
+ timezone("Invalid")
+
+
+def test_timezone_with_offset():
+ tz = timezone(-19800)
+
+ assert isinstance(tz, FixedTimezone)
+ assert tz.name == "-05:30"
diff --git a/tests/tz/test_local_timezone.py b/tests/tz/test_local_timezone.py
new file mode 100644
index 0000000..b8eff6d
--- /dev/null
+++ b/tests/tz/test_local_timezone.py
@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+import os
+import sys
+
+import pytest
+
+from pendulum.tz.local_timezone import _get_unix_timezone
+from pendulum.tz.local_timezone import _get_windows_timezone
+
+
+@pytest.mark.skipif(
+ sys.platform == "win32", reason="Test only available for UNIX systems"
+)
+def test_unix_symlink():
+ # A ZONE setting in the target path of a symbolic linked localtime,
+ # f ex systemd distributions
+ local_path = os.path.join(os.path.split(__file__)[0], "..")
+ tz = _get_unix_timezone(_root=os.path.join(local_path, "fixtures", "tz", "symlink"))
+
+ assert tz.name == "Europe/Paris"
+
+
+@pytest.mark.skipif(
+ sys.platform == "win32", reason="Test only available for UNIX systems"
+)
+def test_unix_clock():
+ # A ZONE setting in the target path of a symbolic linked localtime,
+ # f ex systemd distributions
+ local_path = os.path.join(os.path.split(__file__)[0], "..")
+ tz = _get_unix_timezone(_root=os.path.join(local_path, "fixtures", "tz", "clock"))
+
+ assert tz.name == "Europe/Zurich"
+
+
+@pytest.mark.skipif(sys.platform != "win32", reason="Test only available for Windows")
+def test_windows_timezone():
+ timezone = _get_windows_timezone()
+
+ assert timezone is not None
+
+
+@pytest.mark.skipif(
+ sys.platform == "win32", reason="Test only available for UNIX systems"
+)
+def test_unix_etc_timezone_dir():
+ # Should not fail if `/etc/timezone` is a folder
+ local_path = os.path.join(os.path.split(__file__)[0], "..")
+ root_path = os.path.join(local_path, "fixtures", "tz", "timezone_dir")
+ tz = _get_unix_timezone(_root=root_path)
+
+ assert tz.name == "Europe/Paris"
diff --git a/tests/tz/test_timezone.py b/tests/tz/test_timezone.py
new file mode 100644
index 0000000..655267d
--- /dev/null
+++ b/tests/tz/test_timezone.py
@@ -0,0 +1,447 @@
+from __future__ import annotations
+
+from datetime import datetime
+from datetime import timedelta
+
+import pytest
+
+import pendulum
+
+from pendulum import timezone
+from pendulum.tz import fixed_timezone
+from pendulum.tz.exceptions import AmbiguousTime
+from pendulum.tz.exceptions import NonExistingTime
+from pendulum.utils._compat import zoneinfo
+from tests.conftest import assert_datetime
+
+
+@pytest.fixture(autouse=True)
+def setup():
+ pendulum.tz._tz_cache = {}
+
+ yield
+
+ pendulum.tz._tz_cache = {}
+
+
+def test_basic_convert():
+ dt = datetime(2016, 6, 1, 12, 34, 56, 123456, fold=1)
+ tz = timezone("Europe/Paris")
+ dt = tz.convert(dt)
+
+ assert dt.year == 2016
+ assert dt.month == 6
+ assert dt.day == 1
+ assert dt.hour == 12
+ assert dt.minute == 34
+ assert dt.second == 56
+ assert dt.microsecond == 123456
+ assert dt.tzinfo.name == "Europe/Paris"
+ assert dt.tzinfo.utcoffset(dt) == timedelta(seconds=7200)
+ assert dt.tzinfo.dst(dt) == timedelta(seconds=3600)
+
+
+def test_skipped_time_with_pre_rule():
+ dt = datetime(2013, 3, 31, 2, 30, 45, 123456, fold=0)
+ tz = timezone("Europe/Paris")
+ dt = tz.convert(dt)
+
+ assert dt.year == 2013
+ assert dt.month == 3
+ assert dt.day == 31
+ assert dt.hour == 1
+ assert dt.minute == 30
+ assert dt.second == 45
+ assert dt.microsecond == 123456
+ assert dt.tzinfo.name == "Europe/Paris"
+ assert dt.tzinfo.utcoffset(dt) == timedelta(seconds=3600)
+ assert dt.tzinfo.dst(dt) == timedelta()
+
+
+def test_skipped_time_with_post_rule():
+ dt = datetime(2013, 3, 31, 2, 30, 45, 123456, fold=1)
+ tz = timezone("Europe/Paris")
+ dt = tz.convert(dt)
+
+ assert dt.year == 2013
+ assert dt.month == 3
+ assert dt.day == 31
+ assert dt.hour == 3
+ assert dt.minute == 30
+ assert dt.second == 45
+ assert dt.microsecond == 123456
+ assert dt.tzinfo.name == "Europe/Paris"
+ assert dt.tzinfo.utcoffset(dt) == timedelta(seconds=7200)
+ assert dt.tzinfo.dst(dt) == timedelta(seconds=3600)
+
+
+def test_skipped_time_with_error():
+ dt = datetime(2013, 3, 31, 2, 30, 45, 123456)
+ tz = timezone("Europe/Paris")
+ with pytest.raises(NonExistingTime):
+ tz.convert(dt, raise_on_unknown_times=True)
+
+
+def test_repeated_time():
+ dt = datetime(2013, 10, 27, 2, 30, 45, 123456, fold=1)
+ tz = timezone("Europe/Paris")
+ dt = tz.convert(dt)
+
+ assert dt.year == 2013
+ assert dt.month == 10
+ assert dt.day == 27
+ assert dt.hour == 2
+ assert dt.minute == 30
+ assert dt.second == 45
+ assert dt.microsecond == 123456
+ assert dt.tzinfo.name == "Europe/Paris"
+ assert dt.tzinfo.utcoffset(dt) == timedelta(seconds=3600)
+ assert dt.tzinfo.dst(dt) == timedelta()
+
+
+def test_repeated_time_pre_rule():
+ dt = datetime(2013, 10, 27, 2, 30, 45, 123456, fold=0)
+ tz = timezone("Europe/Paris")
+ dt = tz.convert(dt)
+
+ assert dt.year == 2013
+ assert dt.month == 10
+ assert dt.day == 27
+ assert dt.hour == 2
+ assert dt.minute == 30
+ assert dt.second == 45
+ assert dt.microsecond == 123456
+ assert dt.tzinfo.name == "Europe/Paris"
+ assert dt.tzinfo.utcoffset(dt) == timedelta(seconds=7200)
+ assert dt.tzinfo.dst(dt) == timedelta(seconds=3600)
+
+
+def test_repeated_time_with_error():
+ dt = datetime(2013, 10, 27, 2, 30, 45, 123456)
+ tz = timezone("Europe/Paris")
+ with pytest.raises(AmbiguousTime):
+ tz.convert(dt, raise_on_unknown_times=True)
+
+
+def test_pendulum_create_basic():
+ dt = pendulum.datetime(2016, 6, 1, 12, 34, 56, 123456, tz="Europe/Paris")
+
+ assert_datetime(dt, 2016, 6, 1, 12, 34, 56, 123456)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.offset == 7200
+ assert dt.is_dst()
+
+
+def test_pendulum_create_skipped():
+ dt = pendulum.datetime(2013, 3, 31, 2, 30, 45, 123456, tz="Europe/Paris")
+
+ assert isinstance(dt, pendulum.DateTime)
+ assert_datetime(dt, 2013, 3, 31, 3, 30, 45, 123456)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.tzinfo.utcoffset(dt) == timedelta(seconds=7200)
+ assert dt.tzinfo.dst(dt) == timedelta(seconds=3600)
+
+
+def test_pendulum_create_skipped_with_pre_rule():
+ dt = pendulum.datetime(2013, 3, 31, 2, 30, 45, 123456, tz="Europe/Paris", fold=0)
+
+ assert_datetime(dt, 2013, 3, 31, 1, 30, 45, 123456)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.tzinfo.utcoffset(dt) == timedelta(seconds=3600)
+ assert dt.tzinfo.dst(dt) == timedelta()
+
+
+def test_pendulum_create_skipped_with_error():
+ with pytest.raises(NonExistingTime):
+ pendulum.datetime(
+ 2013,
+ 3,
+ 31,
+ 2,
+ 30,
+ 45,
+ 123456,
+ tz="Europe/Paris",
+ raise_on_unknown_times=True,
+ )
+
+
+def test_pendulum_create_repeated():
+ dt = pendulum.datetime(2013, 10, 27, 2, 30, 45, 123456, tz="Europe/Paris")
+
+ assert_datetime(dt, 2013, 10, 27, 2, 30, 45, 123456)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.tzinfo.utcoffset(dt) == timedelta(seconds=3600)
+ assert dt.tzinfo.dst(dt) == timedelta()
+
+
+def test_pendulum_create_repeated_with_pre_rule():
+ dt = pendulum.datetime(
+ 2013,
+ 10,
+ 27,
+ 2,
+ 30,
+ 45,
+ 123456,
+ tz="Europe/Paris",
+ fold=0,
+ )
+
+ assert_datetime(dt, 2013, 10, 27, 2, 30, 45, 123456)
+ assert dt.timezone_name == "Europe/Paris"
+ assert dt.tzinfo.utcoffset(dt) == timedelta(seconds=7200)
+ assert dt.tzinfo.dst(dt) == timedelta(seconds=3600)
+
+
+def test_pendulum_create_repeated_with_error():
+ with pytest.raises(AmbiguousTime):
+ pendulum.datetime(
+ 2013,
+ 10,
+ 27,
+ 2,
+ 30,
+ 45,
+ 123456,
+ tz="Europe/Paris",
+ raise_on_unknown_times=True,
+ )
+
+
+def test_convert_accept_pendulum_instance():
+ dt = pendulum.datetime(2016, 8, 7, 12, 53, 54)
+ tz = timezone("Europe/Paris")
+ new = tz.convert(dt)
+
+ assert isinstance(new, pendulum.DateTime)
+ assert_datetime(new, 2016, 8, 7, 14, 53, 54)
+
+
+def test_utcoffset():
+ tz = pendulum.timezone("America/Guayaquil")
+ utcoffset = tz.utcoffset(pendulum.now("UTC"))
+ assert utcoffset == timedelta(0, -18000)
+
+
+def test_utcoffset_pre_transition():
+ tz = pendulum.timezone("America/Chicago")
+ utcoffset = tz.utcoffset(datetime(1883, 11, 18))
+ assert utcoffset == timedelta(days=-1, seconds=65364)
+
+
+def test_dst():
+ tz = pendulum.timezone("Europe/Amsterdam")
+ dst = tz.dst(datetime(1940, 7, 1))
+ native_tz = zoneinfo.ZoneInfo("Europe/Amsterdam")
+
+ assert dst == native_tz.dst(datetime(1940, 7, 1))
+
+
+def test_short_timezones_should_not_modify_time():
+ tz = pendulum.timezone("EST")
+ dt = tz.datetime(2017, 6, 15, 14, 0, 0)
+
+ assert dt.year == 2017
+ assert dt.month == 6
+ assert dt.day == 15
+ assert dt.hour == 14
+ assert dt.minute == 0
+ assert dt.second == 0
+
+ tz = pendulum.timezone("HST")
+ dt = tz.datetime(2017, 6, 15, 14, 0, 0)
+
+ assert dt.year == 2017
+ assert dt.month == 6
+ assert dt.day == 15
+ assert dt.hour == 14
+ assert dt.minute == 0
+ assert dt.second == 0
+
+
+def test_after_last_transition():
+ tz = pendulum.timezone("Europe/Paris")
+ dt = tz.datetime(2135, 6, 15, 14, 0, 0)
+
+ assert dt.year == 2135
+ assert dt.month == 6
+ assert dt.day == 15
+ assert dt.hour == 14
+ assert dt.minute == 0
+ assert dt.second == 0
+ assert dt.microsecond == 0
+
+
+@pytest.mark.skip(
+ reason="zoneinfo does not currently support POSIX transition rules to go beyond the last fixed transition."
+)
+def test_on_last_transition():
+ tz = pendulum.timezone("Europe/Paris")
+ dt = pendulum.naive(2037, 10, 25, 2, 30)
+ dt = tz.convert(dt, dst_rule=pendulum.POST_TRANSITION)
+
+ assert dt.year == 2037
+ assert dt.month == 10
+ assert dt.day == 25
+ assert dt.hour == 2
+ assert dt.minute == 30
+ assert dt.second == 0
+ assert dt.microsecond == 0
+ assert dt.utcoffset().total_seconds() == 3600
+
+ dt = pendulum.naive(2037, 10, 25, 2, 30)
+ dt = tz.convert(dt, dst_rule=pendulum.PRE_TRANSITION)
+
+ assert dt.year == 2037
+ assert dt.month == 10
+ assert dt.day == 25
+ assert dt.hour == 2
+ assert dt.minute == 30
+ assert dt.second == 0
+ assert dt.microsecond == 0
+ assert dt.utcoffset().total_seconds() == 7200
+
+
+def test_convert_fold_attribute_is_honored():
+ tz = pendulum.timezone("US/Eastern")
+ dt = datetime(2014, 11, 2, 1, 30)
+
+ new = tz.convert(dt)
+ assert new.strftime("%z") == "-0400"
+
+ new = tz.convert(dt.replace(fold=1))
+ assert new.strftime("%z") == "-0500"
+
+
+def test_utcoffset_fold_attribute_is_honored():
+ tz = pendulum.timezone("US/Eastern")
+ dt = datetime(2014, 11, 2, 1, 30)
+
+ offset = tz.utcoffset(dt)
+
+ assert offset.total_seconds() == -4 * 3600
+
+ offset = tz.utcoffset(dt.replace(fold=1))
+
+ assert offset.total_seconds() == -5 * 3600
+
+
+def test_dst_fold_attribute_is_honored():
+ tz = pendulum.timezone("US/Eastern")
+ dt = datetime(2014, 11, 2, 1, 30)
+
+ offset = tz.dst(dt)
+
+ assert offset.total_seconds() == 3600
+
+ offset = tz.dst(dt.replace(fold=1))
+
+ assert offset.total_seconds() == 0
+
+
+def test_tzname_fold_attribute_is_honored():
+ tz = pendulum.timezone("US/Eastern")
+ dt = datetime(2014, 11, 2, 1, 30)
+
+ name = tz.tzname(dt)
+
+ assert name == "EDT"
+
+ name = tz.tzname(dt.replace(fold=1))
+
+ assert name == "EST"
+
+
+def test_constructor_fold_attribute_is_honored():
+ tz = pendulum.timezone("US/Eastern")
+ dt = datetime(2014, 11, 2, 1, 30, tzinfo=tz)
+
+ assert dt.strftime("%z") == "-0400"
+
+ dt = datetime(2014, 11, 2, 1, 30, tzinfo=tz, fold=1)
+
+ assert dt.strftime("%z") == "-0500"
+
+
+def test_datetime():
+ tz = timezone("Europe/Paris")
+
+ dt = tz.datetime(2013, 3, 24, 1, 30)
+ assert dt.year == 2013
+ assert dt.month == 3
+ assert dt.day == 24
+ assert dt.hour == 1
+ assert dt.minute == 30
+ assert dt.second == 0
+ assert dt.microsecond == 0
+
+ dt = tz.datetime(2013, 3, 31, 2, 30)
+ assert dt.year == 2013
+ assert dt.month == 3
+ assert dt.day == 31
+ assert dt.hour == 3
+ assert dt.minute == 30
+ assert dt.second == 0
+ assert dt.microsecond == 0
+
+
+def test_fixed_timezone():
+ tz = fixed_timezone(19800)
+ tz2 = fixed_timezone(18000)
+ dt = datetime(2016, 11, 26, tzinfo=tz)
+
+ assert tz2.utcoffset(dt).total_seconds() == 18000
+ assert tz2.dst(dt) == timedelta()
+
+
+def test_just_before_last_transition():
+ tz = pendulum.timezone("Asia/Shanghai")
+ dt = datetime(1991, 4, 20, 1, 49, 8, fold=0)
+ dt = tz.convert(dt)
+
+ epoch = datetime(1970, 1, 1, tzinfo=timezone("UTC"))
+ expected = (dt - epoch).total_seconds()
+ assert expected == 672079748.0
+
+
+@pytest.mark.skip(
+ reason="zoneinfo does not currently support POSIX transition rules to go beyond the last fixed transition."
+)
+def test_timezones_are_extended():
+ tz = pendulum.timezone("Europe/Paris")
+ dt = tz.convert(pendulum.naive(2134, 2, 13, 1))
+
+ assert_datetime(dt, 2134, 2, 13, 1)
+ assert dt.utcoffset().total_seconds() == 3600
+ assert dt.dst() == timedelta()
+
+ dt = tz.convert(pendulum.naive(2134, 3, 28, 2, 30))
+
+ assert_datetime(dt, 2134, 3, 28, 3, 30)
+ assert dt.utcoffset().total_seconds() == 7200
+ assert dt.dst() == timedelta(seconds=3600)
+
+ dt = tz.convert(pendulum.naive(2134, 7, 11, 2, 30))
+
+ assert_datetime(dt, 2134, 7, 11, 2, 30)
+ assert dt.utcoffset().total_seconds() == 7200
+ assert dt.dst() == timedelta(seconds=3600)
+
+ dt = tz.convert(pendulum.naive(2134, 10, 31, 2, 30, fold=0))
+
+ assert_datetime(dt, 2134, 10, 31, 2, 30)
+ assert dt.utcoffset().total_seconds() == 7200
+ assert dt.dst() == timedelta(seconds=3600)
+
+ dt = tz.convert(pendulum.naive(2134, 10, 31, 2, 30))
+
+ assert_datetime(dt, 2134, 10, 31, 2, 30)
+ assert dt.utcoffset().total_seconds() == 3600
+ assert dt.dst() == timedelta()
+
+
+def test_repr():
+ tz = timezone("Europe/Paris")
+
+ assert repr(tz) == "Timezone('Europe/Paris')"
diff --git a/tests/tz/test_timezones.py b/tests/tz/test_timezones.py
new file mode 100644
index 0000000..200ed09
--- /dev/null
+++ b/tests/tz/test_timezones.py
@@ -0,0 +1,16 @@
+from __future__ import annotations
+
+import pytest
+
+import pendulum
+
+
+def test_timezones():
+ zones = pendulum.timezones()
+
+ assert "America/Argentina/Buenos_Aires" in zones
+
+
+@pytest.mark.parametrize("zone", list(pendulum.timezones()))
+def test_timezones_are_loadable(zone):
+ pendulum.timezone(zone)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..d45c3e2
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,18 @@
+[tox]
+isolated_build = true
+envlist = py37, py38, py39, py310, pypy3
+
+[testenv]
+whitelist_externals = poetry
+commands =
+ poetry install -v
+ poetry run pytest tests/
+
+[testenv:pypy]
+whitelist_externals =
+ bash
+ poetry
+skip_install = true
+commands =
+ poetry install -v
+ poetry run pytest tests/