diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2021-01-30 07:57:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2021-01-30 07:57:29 +0000 |
commit | a003430ded2dbfbfb48acd3c17f143cbafeee60a (patch) | |
tree | 9016b0b904b32c46542f48de704b3e536b90e1af | |
parent | Initial commit. (diff) | |
download | pendulum-a003430ded2dbfbfb48acd3c17f143cbafeee60a.tar.xz pendulum-a003430ded2dbfbfb48acd3c17f143cbafeee60a.zip |
Adding upstream version 2.1.2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
234 files changed, 29231 insertions, 0 deletions
diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..73ec6b7 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +omit = pendulum/locales/*, + pendulum/_compat.py, + pendulum/__version__.py, + pendulum/_extensions/* + pendulum/parsing/iso8601.py + pendulum/utils/_compat.py @@ -0,0 +1,20 @@ +[flake8] +max-line-length = 88 +ignore = E501, E203, W503 +per-file-ignores = + __init__.py:F401 + pendulum/tz/timezone.py:F811 +exclude = + .git + __pycache__ + setup.py + build + dist + releases + .idea + .venv + .tox + .mypy_cache + .pytest_cache + .vscode + .github 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..3dc1bf5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,132 @@ +name: Release + +on: + push: + tags: + - '*.*.*' + +jobs: + + Linux: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Get tag + id: tag + run: | + echo ::set-output name=tag::${GITHUB_REF#refs/tags/} + - name: Building release + run: | + make linux_release + - name: Upload distributions artifacts + uses: actions/upload-artifact@v1 + with: + name: pendulum-dist + path: dist/wheelhouse + + MacOS: + runs-on: macos-latest + strategy: + matrix: + python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Get tag + id: tag + run: | + echo ::set-output name=tag::${GITHUB_REF#refs/tags/} + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install and set up Poetry + run: | + curl -fsS -o get-poetry.py https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py + python get-poetry.py --preview -y + - name: Build distributions + run: | + source $HOME/.poetry/env + poetry build -vvv + - name: Upload distribution artifacts + uses: actions/upload-artifact@v1 + with: + name: pendulum-dist + path: dist + + Windows: + runs-on: windows-latest + strategy: + matrix: + python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Get tag + id: tag + shell: bash + run: | + echo ::set-output name=tag::${GITHUB_REF#refs/tags/} + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install and setup Poetry + run: | + Invoke-WebRequest https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py -O get-poetry.py + python get-poetry.py --preview -y + - name: Build distributions + run: | + $env:Path += ";$env:Userprofile\.poetry\bin" + poetry build -vvv + - name: Upload distribution artifact + uses: actions/upload-artifact@v1 + with: + name: pendulum-dist + path: dist + + Release: + needs: [Linux, MacOS, Windows] + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Get tag + id: tag + run: | + echo ::set-output name=tag::${GITHUB_REF#refs/tags/} + - name: Download distribution artifact + uses: actions/download-artifact@master + with: + name: pendulum-dist + path: dist + - name: Install and set up Poetry + run: | + curl -fsS -o get-poetry.py https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py + python get-poetry.py --preview -y + - name: Set up cache + uses: actions/cache@v1 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} + - name: Check distributions + run: | + ls -la dist + - name: Publish to PyPI + env: + POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} + run: | + source $HOME/.poetry/env + poetry publish + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + with: + tag_name: ${{ steps.tag.outputs.tag }} + release_name: ${{ steps.tag.outputs.tag }} + draft: false + prerelease: false diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..2e8b58e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,154 @@ +name: Tests + +on: [push, pull_request] + +jobs: + Linting: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Linting + run: | + pip install pre-commit + pre-commit run --all-files + Linux: + needs: Linting + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [2.7, 3.5, 3.6, 3.7, 3.8, pypy3] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + 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 and set up Poetry + run: | + curl -fsS -o get-poetry.py https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py + python get-poetry.py --preview -y + source $HOME/.poetry/env + poetry config virtualenvs.in-project true + - name: Set up cache + uses: actions/cache@v1 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} + - name: Upgrade pip + run: | + source $HOME/.poetry/env + poetry run python -m pip install pip -U + - name: Install dependencies + run: | + source $HOME/.poetry/env + poetry install -vvv + - name: Test Pure Python + run: | + source $HOME/.poetry/env + PENDULUM_EXTENSIONS=0 poetry run pytest -q tests + - name: Test + run: | + source $HOME/.poetry/env + poetry run pytest -q tests + poetry install + + MacOS: + needs: Linting + runs-on: macos-latest + strategy: + matrix: + python-version: [2.7, 3.5, 3.6, 3.7, 3.8, pypy3] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + 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 and set up Poetry + run: | + curl -fsS -o get-poetry.py https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py + python get-poetry.py --preview -y + source $HOME/.poetry/env + poetry config virtualenvs.in-project true + - name: Set up cache + uses: actions/cache@v1 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-fix-${{ hashFiles('**/poetry.lock') }} + - name: Upgrade pip + run: | + source $HOME/.poetry/env + poetry run python -m pip install pip -U + - name: Install dependencies + run: | + source $HOME/.poetry/env + poetry install -vvv + - name: Test Pure Python + run: | + source $HOME/.poetry/env + PENDULUM_EXTENSIONS=0 poetry run pytest -q tests + - name: Test + run: | + source $HOME/.poetry/env + poetry run pytest -q tests + Windows: + needs: Linting + runs-on: windows-latest + strategy: + matrix: + python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Get full python version + id: full-python-version + shell: bash + run: | + echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") + - name: Install and setup Poetry + run: | + Invoke-WebRequest https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py -O get-poetry.py + python get-poetry.py --preview -y + $env:Path += ";$env:Userprofile\.poetry\bin" + poetry config virtualenvs.in-project true + - name: Set up cache + uses: actions/cache@v1 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} + - name: Upgrade pip + run: | + $env:Path += ";$env:Userprofile\.poetry\bin" + poetry run python -m pip install pip -U + - name: Install dependencies + run: | + $env:Path += ";$env:Userprofile\.poetry\bin" + poetry install -vvv + - name: Test Pure Python + run: | + $env:Path += ";$env:Userprofile\.poetry\bin" + $env:PENDULUM_EXTENSIONS = "0" + poetry run pytest -q tests + - name: Test + run: | + $env:Path += ";$env:Userprofile\.poetry\bin" + 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..4266921 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +repos: + - repo: https://github.com/psf/black + rev: stable + hooks: + - id: black + + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.8 + hooks: + - id: flake8 + + - repo: https://github.com/timothycrosley/isort + rev: 4.3.21 + hooks: + - id: isort + additional_dependencies: [toml] + exclude: ^.*/?setup\.py$ + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: trailing-whitespace + exclude: ^tests/.*/fixtures/.* + - id: end-of-file-fixer + exclude: ^tests/.*/fixtures/.* + - id: debug-statements diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..02f2480 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,162 @@ +# Change Log + +## [2.1.2] - 2020-07-24 + +### Fixed + +- Fixed errors when trying to build Pendulum from source ([#489](https://github.com/sdispater/pendulum/pull/489)). + + +## [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/2.1.2...master +[2.1.2]: https://github.com/sdispater/pendulum/releases/tag/2.1.2 +[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 @@ -0,0 +1,20 @@ +Copyright (c) 2015 Sébastien Eustace + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/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/README.rst b/README.rst new file mode 100644 index 0000000..313d4c9 --- /dev/null +++ b/README.rst @@ -0,0 +1,224 @@ +Pendulum +######## + +.. image:: https://img.shields.io/pypi/v/pendulum.svg + :target: https://pypi.python.org/pypi/pendulum + +.. image:: https://img.shields.io/pypi/l/pendulum.svg + :target: https://pypi.python.org/pypi/pendulum + +.. image:: https://img.shields.io/codecov/c/github/sdispater/pendulum/master.svg + :target: https://codecov.io/gh/sdispater/pendulum/branch/master + +.. image:: https://travis-ci.org/sdispater/pendulum.svg + :alt: Pendulum Build status + :target: https://travis-ci.org/sdispater/pendulum + +Python datetimes made easy. + +Supports Python **2.7** and **3.4+**. + + +.. code-block:: python + + >>> import pendulum + + >>> now_in_paris = pendulum.now('Europe/Paris') + >>> now_in_paris + '2016-07-04T00:49:58.502116+02:00' + + # Seamless timezone switching + >>> now_in_paris.in_timezone('UTC') + '2016-07-03T22:49:58.502116+00:00' + + >>> tomorrow = pendulum.now().add(days=1) + >>> last_week = pendulum.now().subtract(weeks=1) + + >>> past = pendulum.now().subtract(minutes=2) + >>> past.diff_for_humans() + >>> '2 minutes ago' + + >>> delta = past - last_week + >>> delta.hours + 23 + >>> delta.in_words(locale='en') + '6 days 23 hours 58 minutes' + + # Proper handling of datetime normalization + >>> pendulum.datetime(2013, 3, 31, 2, 30, tz='Europe/Paris') + '2013-03-31T03:30:00+02:00' # 2:30 does not exist (Skipped time) + + # Proper handling of dst transitions + >>> just_before = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, tz='Europe/Paris') + '2013-03-31T01:59:59.999999+01:00' + >>> just_before.add(microseconds=1) + '2013-03-31T03:00:00+02:00' + + +Why Pendulum? +============= + +Native ``datetime`` instances are enough for basic cases but when you face more complex use-cases +they often show limitations and are not so intuitive to work with. +``Pendulum`` provides a cleaner and more easy to use API while still relying on the standard library. +So it's still ``datetime`` but better. + +Unlike other datetime libraries for Python, Pendulum is a drop-in replacement +for the standard ``datetime`` class (it inherits from it), so, basically, you can replace all your ``datetime`` +instances by ``DateTime`` instances in you code (exceptions exist for libraries that check +the type of the objects by using the ``type`` function like ``sqlite3`` or ``PyMySQL`` for instance). + +It also removes the notion of naive datetimes: each ``Pendulum`` instance is timezone-aware +and by default in ``UTC`` for ease of use. + +Pendulum also improves the standard ``timedelta`` class by providing more intuitive methods and properties. + + +Why not Arrow? +============== + +Arrow is the most popular datetime library for Python right now, however its behavior +and API can be erratic and unpredictable. The ``get()`` method can receive pretty much anything +and it will try its best to return something while silently failing to handle some cases: + +.. code-block:: python + + arrow.get('2016-1-17') + # <Arrow [2016-01-01T00:00:00+00:00]> + + pendulum.parse('2016-1-17') + # <Pendulum [2016-01-17T00:00:00+00:00]> + + arrow.get('20160413') + # <Arrow [1970-08-22T08:06:53+00:00]> + + pendulum.parse('20160413') + # <Pendulum [2016-04-13T00:00:00+00:00]> + + arrow.get('2016-W07-5') + # <Arrow [2016-01-01T00:00:00+00:00]> + + pendulum.parse('2016-W07-5') + # <Pendulum [2016-02-19T00:00:00+00:00]> + + # Working with DST + just_before = arrow.Arrow(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris') + just_after = just_before.replace(microseconds=1) + '2013-03-31T02:00:00+02:00' + # Should be 2013-03-31T03:00:00+02:00 + + (just_after.to('utc') - just_before.to('utc')).total_seconds() + -3599.999999 + # Should be 1e-06 + + just_before = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris') + just_after = just_before.add(microseconds=1) + '2013-03-31T03:00:00+02:00' + + (just_after.in_timezone('utc') - just_before.in_timezone('utc')).total_seconds() + 1e-06 + +Those are a few examples showing that Arrow cannot always be trusted to have a consistent +behavior with the data you are passing to it. + + +Limitations +=========== + +Even though the ``DateTime`` class is a subclass of ``datetime`` there are some rare cases where +it can't replace the native class directly. Here is a list (non-exhaustive) of the reported cases with +a possible solution, if any: + +* ``sqlite3`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter: + +.. code-block:: python + + from pendulum import DateTime + from sqlite3 import register_adapter + + register_adapter(DateTime, lambda val: val.isoformat(' ')) + +* ``mysqlclient`` (former ``MySQLdb``) and ``PyMySQL`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter: + +.. code-block:: python + + import MySQLdb.converters + import pymysql.converters + + from pendulum import DateTime + + MySQLdb.converters.conversions[DateTime] = MySQLdb.converters.DateTime2literal + pymysql.converters.conversions[DateTime] = pymysql.converters.escape_datetime + +* ``django`` will use the ``isoformat()`` method to store datetimes in the database. However since ``pendulum`` is always timezone aware the offset information will always be returned by ``isoformat()`` raising an error, at least for MySQL databases. To work around it you can either create your own ``DateTimeField`` or use the previous workaround for ``MySQLdb``: + +.. code-block:: python + + from django.db.models import DateTimeField as BaseDateTimeField + from pendulum import DateTime + + + class DateTimeField(BaseDateTimeField): + + def value_to_string(self, obj): + val = self.value_from_object(obj) + + if isinstance(value, DateTime): + return value.to_datetime_string() + + return '' if val is None else val.isoformat() + + +Resources +========= + +* `Official Website <https://pendulum.eustace.io>`_ +* `Documentation <https://pendulum.eustace.io/docs/>`_ +* `Issue Tracker <https://github.com/sdispater/pendulum/issues>`_ + + +Contributing +============ + +Contributions are welcome, especially with localization. + +Getting started +--------------- + +To work on the Pendulum codebase, you'll want to clone the project locally +and install the required depedendencies via `poetry <https://poetry.eustace.io>`_. + +.. code-block:: bash + + $ git clone git@github.com:sdispater/pendulum.git + $ poetry install + +Localization +------------ + +If you want to help with localization, there are two different cases: the locale already exists +or not. + +If the locale does not exist you will need to create it by using the ``clock`` utility: + +.. code-block:: bash + + ./clock locale create <your-locale> + +It will generate a directory in ``pendulum/locales`` named after your locale, with the following +structure: + +.. code-block:: text + + <your-locale>/ + - custom.py + - locale.py + +The ``locale.py`` file must not be modified. It contains the translations provided by +the CLDR database. + +The ``custom.py`` file is the one you want to modify. It contains the data needed +by Pendulum that are not provided by the CLDR database. You can take the `en <https://github.com/sdispater/pendulum/tree/master/pendulum/locales/en/custom.py>`_ +data as a reference to see which data is needed. + +You should also add tests for the created or modified locale. diff --git a/build-wheels.sh b/build-wheels.sh new file mode 100755 index 0000000..af63d1b --- /dev/null +++ b/build-wheels.sh @@ -0,0 +1,24 @@ +#!/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 + 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 new file mode 100644 index 0000000..885b1c3 --- /dev/null +++ b/build.py @@ -0,0 +1,87 @@ +import os +import shutil +import sys + +from distutils.command.build_ext import build_ext +from distutils.core import Distribution +from distutils.core import Extension +from distutils.errors import CCompilerError +from distutils.errors import DistutilsExecError +from distutils.errors import DistutilsPlatformError + + +# C Extensions +with_extensions = os.getenv("PENDULUM_EXTENSIONS", None) + +if with_extensions == "1" or with_extensions is None: + with_extensions = True + +if with_extensions == "0" or hasattr(sys, "pypy_version_info"): + with_extensions = False + +extensions = [] +if with_extensions: + extensions = [ + Extension("pendulum._extensions._helpers", ["pendulum/_extensions/_helpers.c"]), + Extension("pendulum.parsing._iso8601", ["pendulum/parsing/_iso8601.c"]), + ] + + +class BuildFailed(Exception): + + pass + + +class ExtBuilder(build_ext): + # This class allows C extension building to fail. + + built_extensions = [] + + def run(self): + try: + build_ext.run(self) + except (DistutilsPlatformError, FileNotFoundError): + print( + " Unable to build the C extensions, " + "Pendulum will use the pure python code instead." + ) + + def build_extension(self, ext): + try: + build_ext.build_extension(self, ext) + except (CCompilerError, DistutilsExecError, DistutilsPlatformError, ValueError): + print( + ' Unable to build the "{}" C extension, ' + "Pendulum will use the pure python version of the extension.".format( + ext.name + ) + ) + + +def build(setup_kwargs): + """ + This function is mandatory in order to build the extensions. + """ + distribution = Distribution({"name": "pendulum", "ext_modules": extensions}) + distribution.package_dir = "pendulum" + + cmd = ExtBuilder(distribution) + cmd.ensure_finalized() + cmd.run() + + # Copy built extensions back to the project + for output in cmd.get_outputs(): + relative_extension = os.path.relpath(output, cmd.build_lib) + if not os.path.exists(output): + continue + + shutil.copyfile(output, relative_extension) + mode = os.stat(relative_extension).st_mode + mode |= (mode & 0o444) >> 2 + os.chmod(relative_extension, mode) + + return setup_kwargs + + +if __name__ == "__main__": + build({}) @@ -0,0 +1,302 @@ +#!/usr/bin/env python +from __future__ import unicode_literals + +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 import Application +from cleo import Command +from cleo 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 = "(%s == %s and %s)" % (expr, expr, code) + return code + + +class LocaleCreate(Command): + + name = "create" + description = "Creates locale translations." + + arguments = [argument("locales", "Locales to dump.", optional=False, multiple=True)] + + TEMPLATE = """# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +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 = """# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +\"\"\" +{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("<error>Locale [{}] does not exist.</error>".format(locale)) + continue + + self.line("<info>Generating <comment>{}</> locale.</>".format(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["duration-{}".format(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("%s%r: %s,\n" % (" " * tab, k, v)) + s.append("%s}" % (" " * (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("'%s' if %s else " % (tag, to_py(ast))) + 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( + "Invalid length for field: %r" % (fieldchar * fieldnum) + ) + result.append( + self.TOKENS_MAP.get(fieldchar * fieldnum, fieldchar * fieldnum) + ) + else: + raise NotImplementedError("Unknown token type: %s" % tok_type) + + return "".join(result) + + +class LocaleRecreate(Command): + + name = "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(l)) for l in locales] + + self.call("locale:create", [("locales", locales)]) + + +class LocaleCommand(Command): + + name = "locale" + description = "Locale related commands." + + commands = [LocaleCreate()] + + def handle(self): + self.call("help", self._config.name) + + +class WindowsTzDump(Command): + + name = "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(list(raw_tznames.keys())) + + tznames = {} + for name in sorted_names: + tznames[name] = raw_tznames[name] + + mapping = json.dumps(tznames, indent=4) + mapping = "windows_timezones = " + mapping.replace('"', "'") + "\n" + + with open(os.path.join(self.MAPPING_DIR, "windows.py"), "w") as f: + f.write(mapping) + + +class WindowsCommand(Command): + + name = "windows" + description = "Windows related commands." + + commands = [WindowsTzDump()] + + def handle(self): + self.call("help", self._config.name) + + +app = Application("clock", __version__) +app.add(LocaleCommand()) +app.add(WindowsCommand()) + + +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..bc3fb50 --- /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, 2, 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..052966e --- /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 compatiblity 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` and `seconds` + +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..ec6e021 --- /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, 24 | +| | H | 0, 1, 2 ... 23, 24 | +| | 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/pendulum/__init__.py b/pendulum/__init__.py new file mode 100644 index 0000000..bb1e0ca --- /dev/null +++ b/pendulum/__init__.py @@ -0,0 +1,315 @@ +from __future__ import absolute_import + +import datetime as _datetime + +from typing import Optional +from typing import Union + +from .__version__ import __version__ +from .constants import DAYS_PER_WEEK +from .constants import FRIDAY +from .constants import HOURS_PER_DAY +from .constants import MINUTES_PER_HOUR +from .constants import MONDAY +from .constants import MONTHS_PER_YEAR +from .constants import SATURDAY +from .constants import SECONDS_PER_DAY +from .constants import SECONDS_PER_HOUR +from .constants import SECONDS_PER_MINUTE +from .constants import SUNDAY +from .constants import THURSDAY +from .constants import TUESDAY +from .constants import WEDNESDAY +from .constants import WEEKS_PER_YEAR +from .constants import YEARS_PER_CENTURY +from .constants import YEARS_PER_DECADE +from .date import Date +from .datetime import DateTime +from .duration import Duration +from .formatting import Formatter +from .helpers import format_diff +from .helpers import get_locale +from .helpers import get_test_now +from .helpers import has_test_now +from .helpers import locale +from .helpers import set_locale +from .helpers import set_test_now +from .helpers import test +from .helpers import week_ends_at +from .helpers import week_starts_at +from .parser import parse +from .period import Period +from .time import Time +from .tz import POST_TRANSITION +from .tz import PRE_TRANSITION +from .tz import TRANSITION_ERROR +from .tz import UTC +from .tz import local_timezone +from .tz import set_local_timezone +from .tz import test_local_timezone +from .tz import timezone +from .tz import timezones +from .tz.timezone import Timezone as _Timezone +from .utils._compat import _HAS_FOLD + + +_TEST_NOW = None # type: Optional[DateTime] +_LOCALE = "en" +_WEEK_STARTS_AT = MONDAY +_WEEK_ENDS_AT = SUNDAY + +_formatter = Formatter() + + +def _safe_timezone(obj): + # type: (Optional[Union[str, float, _datetime.tzinfo, _Timezone]]) -> _Timezone + """ + Creates a timezone instance + from a string, Timezone, TimezoneInfo or integer offset. + """ + if isinstance(obj, _Timezone): + return obj + + if obj is None or obj == "local": + return local_timezone() + + if isinstance(obj, (int, float)): + obj = int(obj * 60 * 60) + elif isinstance(obj, _datetime.tzinfo): + # pytz + if hasattr(obj, "localize"): + obj = obj.zone + elif obj.tzname(None) == "UTC": + return UTC + else: + offset = obj.utcoffset(None) + + if offset is None: + offset = _datetime.timedelta(0) + + obj = int(offset.total_seconds()) + + return timezone(obj) + + +# Public API +def datetime( + year, # type: int + month, # type: int + day, # type: int + hour=0, # type: int + minute=0, # type: int + second=0, # type: int + microsecond=0, # type: int + tz=UTC, # type: Optional[Union[str, float, _Timezone]] + dst_rule=POST_TRANSITION, # type: str +): # type: (...) -> DateTime + """ + Creates a new DateTime instance from a specific date and time. + """ + if tz is not None: + tz = _safe_timezone(tz) + + if not _HAS_FOLD: + dt = naive(year, month, day, hour, minute, second, microsecond) + else: + dt = _datetime.datetime(year, month, day, hour, minute, second, microsecond) + if tz is not None: + dt = tz.convert(dt, dst_rule=dst_rule) + + return DateTime( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=dt.tzinfo, + fold=dt.fold, + ) + + +def local( + year, month, day, hour=0, minute=0, second=0, microsecond=0 +): # type: (int, int, int, int, int, int, int) -> DateTime + """ + Return a DateTime in the local timezone. + """ + return datetime( + year, month, day, hour, minute, second, microsecond, tz=local_timezone() + ) + + +def naive( + year, month, day, hour=0, minute=0, second=0, microsecond=0 +): # type: (int, int, int, int, int, int, int) -> DateTime + """ + Return a naive DateTime. + """ + return DateTime(year, month, day, hour, minute, second, microsecond) + + +def date(year, month, day): # type: (int, int, int) -> Date + """ + Create a new Date instance. + """ + return Date(year, month, day) + + +def time(hour, minute=0, second=0, microsecond=0): # type: (int, int, int, int) -> Time + """ + Create a new Time instance. + """ + return Time(hour, minute, second, microsecond) + + +def instance( + dt, tz=UTC +): # type: (_datetime.datetime, Optional[Union[str, _Timezone]]) -> DateTime + """ + Create a DateTime instance from a datetime one. + """ + if not isinstance(dt, _datetime.datetime): + raise ValueError("instance() only accepts datetime objects.") + + if isinstance(dt, DateTime): + return dt + + tz = dt.tzinfo or tz + + # Checking for pytz/tzinfo + if isinstance(tz, _datetime.tzinfo) and not isinstance(tz, _Timezone): + # pytz + if hasattr(tz, "localize") and tz.zone: + tz = tz.zone + else: + # We have no sure way to figure out + # the timezone name, we fallback + # on a fixed offset + tz = tz.utcoffset(dt).total_seconds() / 3600 + + return datetime( + dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond, tz=tz + ) + + +def now(tz=None): # type: (Optional[Union[str, _Timezone]]) -> DateTime + """ + Get a DateTime instance for the current date and time. + """ + if has_test_now(): + test_instance = get_test_now() + _tz = _safe_timezone(tz) + + if tz is not None and _tz != test_instance.timezone: + test_instance = test_instance.in_tz(_tz) + + return test_instance + + if tz is None or tz == "local": + dt = _datetime.datetime.now(local_timezone()) + elif tz is UTC or tz == "UTC": + dt = _datetime.datetime.now(UTC) + else: + dt = _datetime.datetime.now(UTC) + tz = _safe_timezone(tz) + dt = tz.convert(dt) + + return DateTime( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=dt.tzinfo, + fold=dt.fold if _HAS_FOLD else 0, + ) + + +def today(tz="local"): # type: (Union[str, _Timezone]) -> DateTime + """ + Create a DateTime instance for today. + """ + return now(tz).start_of("day") + + +def tomorrow(tz="local"): # type: (Union[str, _Timezone]) -> DateTime + """ + Create a DateTime instance for today. + """ + return today(tz).add(days=1) + + +def yesterday(tz="local"): # type: (Union[str, _Timezone]) -> DateTime + """ + Create a DateTime instance for today. + """ + return today(tz).subtract(days=1) + + +def from_format( + string, fmt, tz=UTC, locale=None, # noqa +): # type: (str, str, Union[str, _Timezone], Optional[str]) -> DateTime + """ + Creates a DateTime instance from a specific format. + """ + parts = _formatter.parse(string, fmt, now(), locale=locale) + if parts["tz"] is None: + parts["tz"] = tz + + return datetime(**parts) + + +def from_timestamp( + timestamp, tz=UTC +): # type: (Union[int, float], Union[str, _Timezone]) -> DateTime + """ + Create a DateTime instance from a timestamp. + """ + dt = _datetime.datetime.utcfromtimestamp(timestamp) + + dt = datetime( + dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond + ) + + if tz is not UTC or tz != "UTC": + dt = dt.in_timezone(tz) + + return dt + + +def duration( + days=0, # type: float + seconds=0, # type: float + microseconds=0, # type: float + milliseconds=0, # type: float + minutes=0, # type: float + hours=0, # type: float + weeks=0, # type: float + years=0, # type: float + months=0, # type: float +): # type: (...) -> Duration + """ + Create a Duration instance. + """ + return Duration( + days=days, + seconds=seconds, + microseconds=microseconds, + milliseconds=milliseconds, + minutes=minutes, + hours=hours, + weeks=weeks, + years=years, + months=months, + ) + + +def period(start, end, absolute=False): # type: (DateTime, DateTime, bool) -> Period + """ + Create a Period instance. + """ + return Period(start, end, absolute=absolute) diff --git a/pendulum/__version__.py b/pendulum/__version__.py new file mode 100644 index 0000000..4eabd0b --- /dev/null +++ b/pendulum/__version__.py @@ -0,0 +1 @@ +__version__ = "2.1.2" diff --git a/pendulum/_extensions/__init__.py b/pendulum/_extensions/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/_extensions/__init__.py diff --git a/pendulum/_extensions/_helpers.c b/pendulum/_extensions/_helpers.c new file mode 100644 index 0000000..b9310f2 --- /dev/null +++ b/pendulum/_extensions/_helpers.c @@ -0,0 +1,930 @@ +/* ------------------------------------------------------------------------- */ + +#include <Python.h> +#include <datetime.h> +#include <structmember.h> +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +/* ------------------------------------------------------------------------- */ + +#define EPOCH_YEAR 1970 + +#define DAYS_PER_N_YEAR 365 +#define DAYS_PER_L_YEAR 366 + +#define USECS_PER_SEC 1000000 + +#define SECS_PER_MIN 60 +#define SECS_PER_HOUR (60 * SECS_PER_MIN) +#define SECS_PER_DAY (SECS_PER_HOUR * 24) + +// 400-year chunks always have 146097 days (20871 weeks). +#define DAYS_PER_400_YEARS 146097L +#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)SECS_PER_DAY) + +// The number of seconds in an aligned 100-year chunk, for those that +// do not begin with a leap year and those that do respectively. +const int64_t SECS_PER_100_YEARS[2] = { + (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY, + (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * DAYS_PER_L_YEAR) * SECS_PER_DAY}; + +// The number of seconds in an aligned 4-year chunk, for those that +// do not begin with a leap year and those that do respectively. +const int32_t SECS_PER_4_YEARS[2] = { + (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY, + (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY}; + +// The number of seconds in non-leap and leap years respectively. +const int32_t SECS_PER_YEAR[2] = { + DAYS_PER_N_YEAR * SECS_PER_DAY, + DAYS_PER_L_YEAR *SECS_PER_DAY}; + +#define MONTHS_PER_YEAR 12 + +// The month lengths in non-leap and leap years respectively. +const int32_t DAYS_PER_MONTHS[2][13] = { + {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, + {-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}}; + +// The day offsets of the beginning of each (1-based) month in non-leap +// and leap years respectively. +// For example, in a leap year there are 335 days before December. +const int32_t MONTHS_OFFSETS[2][14] = { + {-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}, + {-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}}; + +const int DAY_OF_WEEK_TABLE[12] = { + 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}; + +#define TM_SUNDAY 0 +#define TM_MONDAY 1 +#define TM_TUESDAY 2 +#define TM_WEDNESDAY 3 +#define TM_THURSDAY 4 +#define TM_FRIDAY 5 +#define TM_SATURDAY 6 + +#define TM_JANUARY 0 +#define TM_FEBRUARY 1 +#define TM_MARCH 2 +#define TM_APRIL 3 +#define TM_MAY 4 +#define TM_JUNE 5 +#define TM_JULY 6 +#define TM_AUGUST 7 +#define TM_SEPTEMBER 8 +#define TM_OCTOBER 9 +#define TM_NOVEMBER 10 +#define TM_DECEMBER 11 + +/* ------------------------------------------------------------------------- */ + +int _p(int y) +{ + return y + y / 4 - y / 100 + y / 400; +} + +int _is_leap(int year) +{ + return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); +} + +int _is_long_year(int year) +{ + return (_p(year) % 7 == 4) || (_p(year - 1) % 7 == 3); +} + +int _week_day(int year, int month, int day) +{ + int y; + int w; + + y = year - (month < 3); + + w = (_p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7; + + if (!w) + { + w = 7; + } + + return w; +} + +int _days_in_year(int year) +{ + if (_is_leap(year)) + { + return DAYS_PER_L_YEAR; + } + + return DAYS_PER_N_YEAR; +} + +int _day_number(int year, int month, int day) +{ + month = (month + 9) % 12; + year = year - month / 10; + + return ( + 365 * year + year / 4 - year / 100 + year / 400 + (month * 306 + 5) / 10 + (day - 1)); +} + +int _get_offset(PyObject *dt) +{ + PyObject *tzinfo; + PyObject *offset; + + tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo; + + if (tzinfo != Py_None) + { + offset = PyObject_CallMethod(tzinfo, "utcoffset", "O", dt); + + return PyDateTime_DELTA_GET_DAYS(offset) * SECS_PER_DAY + PyDateTime_DELTA_GET_SECONDS(offset); + } + + return 0; +} + +int _has_tzinfo(PyObject *dt) +{ + return ((_PyDateTime_BaseTZInfo *)(dt))->hastzinfo; +} + +char *_get_tz_name(PyObject *dt) +{ + PyObject *tzinfo; + char *tz = ""; + + tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo; + + if (tzinfo != Py_None) + { + if (PyObject_HasAttrString(tzinfo, "name")) + { + // Pendulum timezone + tz = (char *)PyUnicode_AsUTF8( + PyObject_GetAttrString(tzinfo, "name")); + } + else if (PyObject_HasAttrString(tzinfo, "zone")) + { + // pytz timezone + tz = (char *)PyUnicode_AsUTF8( + PyObject_GetAttrString(tzinfo, "zone")); + } + } + + return tz; +} + +/* ------------------------ Custom Types ------------------------------- */ + +/* + * class Diff(): + */ +typedef struct +{ + PyObject_HEAD int years; + int months; + int days; + int hours; + int minutes; + int seconds; + int microseconds; + int total_days; +} Diff; + +/* + * def __init__(self, years, months, days, hours, minutes, seconds, microseconds, total_days): + * self.years = years + * # ... +*/ +static int Diff_init(Diff *self, PyObject *args, PyObject *kwargs) +{ + int years; + int months; + int days; + int hours; + int minutes; + int seconds; + int microseconds; + int total_days; + + if (!PyArg_ParseTuple(args, "iiiiiii", &years, &months, &days, &hours, &minutes, &seconds, µseconds, &total_days)) + return -1; + + self->years = years; + self->months = months; + self->days = days; + self->hours = hours; + self->minutes = minutes; + self->seconds = seconds; + self->microseconds = microseconds; + self->total_days = total_days; + + return 0; +} + +/* + * def __repr__(self): + * return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format( + * self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds + * ) + */ +static PyObject *Diff_repr(Diff *self) +{ + char repr[82] = {0}; + + sprintf( + repr, + "%d years %d months %d days %d hours %d minutes %d seconds %d microseconds", + self->years, + self->months, + self->days, + self->hours, + self->minutes, + self->seconds, + self->microseconds); + + return PyUnicode_FromString(repr); +} + +/* + * Instantiate new Diff_type object + * Skip overhead of calling PyObject_New and PyObject_Init. + * Directly allocate object. + */ +static PyObject *new_diff_ex(int years, int months, int days, int hours, int minutes, int seconds, int microseconds, int total_days, PyTypeObject *type) +{ + Diff *self = (Diff *)(type->tp_alloc(type, 0)); + + if (self != NULL) + { + self->years = years; + self->months = months; + self->days = days; + self->hours = hours; + self->minutes = minutes; + self->seconds = seconds; + self->microseconds = microseconds; + self->total_days = total_days; + } + + return (PyObject *)self; +} + +/* + * Class member / class attributes + */ +static PyMemberDef Diff_members[] = { + {"years", T_INT, offsetof(Diff, years), 0, "years in diff"}, + {"months", T_INT, offsetof(Diff, months), 0, "months in diff"}, + {"days", T_INT, offsetof(Diff, days), 0, "days in diff"}, + {"hours", T_INT, offsetof(Diff, hours), 0, "hours in diff"}, + {"minutes", T_INT, offsetof(Diff, minutes), 0, "minutes in diff"}, + {"seconds", T_INT, offsetof(Diff, seconds), 0, "seconds in diff"}, + {"microseconds", T_INT, offsetof(Diff, microseconds), 0, "microseconds in diff"}, + {"total_days", T_INT, offsetof(Diff, total_days), 0, "total days in diff"}, + {NULL}}; + +static PyTypeObject Diff_type = { + PyVarObject_HEAD_INIT(NULL, 0) "PreciseDiff", /* tp_name */ + sizeof(Diff), /* tp_basicsize */ + 0, /* tp_itemsize */ + 0, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_as_async */ + (reprfunc)Diff_repr, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + (reprfunc)Diff_repr, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ + "Precise difference between two datetime objects", /* tp_doc */ +}; + +#define new_diff(years, months, days, hours, minutes, seconds, microseconds, total_days) new_diff_ex(years, months, days, hours, minutes, seconds, microseconds, total_days, &Diff_type) + +/* -------------------------- Functions --------------------------*/ + +PyObject *is_leap(PyObject *self, PyObject *args) +{ + PyObject *leap; + int year; + + if (!PyArg_ParseTuple(args, "i", &year)) + { + PyErr_SetString( + PyExc_ValueError, "Invalid parameters"); + return NULL; + } + + leap = PyBool_FromLong(_is_leap(year)); + + return leap; +} + +PyObject *is_long_year(PyObject *self, PyObject *args) +{ + PyObject *is_long; + int year; + + if (!PyArg_ParseTuple(args, "i", &year)) + { + PyErr_SetString( + PyExc_ValueError, "Invalid parameters"); + return NULL; + } + + is_long = PyBool_FromLong(_is_long_year(year)); + + return is_long; +} + +PyObject *week_day(PyObject *self, PyObject *args) +{ + PyObject *wd; + int year; + int month; + int day; + + if (!PyArg_ParseTuple(args, "iii", &year, &month, &day)) + { + PyErr_SetString( + PyExc_ValueError, "Invalid parameters"); + return NULL; + } + + wd = PyLong_FromLong(_week_day(year, month, day)); + + return wd; +} + +PyObject *days_in_year(PyObject *self, PyObject *args) +{ + PyObject *ndays; + int year; + + if (!PyArg_ParseTuple(args, "i", &year)) + { + PyErr_SetString( + PyExc_ValueError, "Invalid parameters"); + return NULL; + } + + ndays = PyLong_FromLong(_days_in_year(year)); + + return ndays; +} + +PyObject *timestamp(PyObject *self, PyObject *args) +{ + int64_t result; + PyObject *dt; + + if (!PyArg_ParseTuple(args, "O", &dt)) + { + PyErr_SetString( + PyExc_ValueError, "Invalid parameters"); + return NULL; + } + + int year = (double)PyDateTime_GET_YEAR(dt); + int month = PyDateTime_GET_MONTH(dt); + int day = PyDateTime_GET_DAY(dt); + int hour = PyDateTime_DATE_GET_HOUR(dt); + int minute = PyDateTime_DATE_GET_MINUTE(dt); + int second = PyDateTime_DATE_GET_SECOND(dt); + + result = (year - 1970) * 365 + MONTHS_OFFSETS[0][month]; + result += (int)floor((double)(year - 1968) / 4); + result -= (year - 1900) / 100; + result += (year - 1600) / 400; + + if (_is_leap(year) && month < 3) + { + result -= 1; + } + + result += day - 1; + result *= 24; + result += hour; + result *= 60; + result += minute; + result *= 60; + result += second; + + return PyLong_FromSsize_t(result); +} + +PyObject *local_time(PyObject *self, PyObject *args) +{ + double unix_time; + int32_t utc_offset; + int32_t year; + int32_t microsecond; + int64_t seconds; + int32_t leap_year; + int64_t sec_per_100years; + int64_t sec_per_4years; + int32_t sec_per_year; + int32_t month; + int32_t day; + int32_t month_offset; + int32_t hour; + int32_t minute; + int32_t second; + + if (!PyArg_ParseTuple(args, "dii", &unix_time, &utc_offset, µsecond)) + { + PyErr_SetString( + PyExc_ValueError, "Invalid parameters"); + return NULL; + } + + year = EPOCH_YEAR; + seconds = (int64_t)floor(unix_time); + + // Shift to a base year that is 400-year aligned. + if (seconds >= 0) + { + seconds -= 10957L * SECS_PER_DAY; + year += 30; // == 2000; + } + else + { + seconds += (int64_t)(146097L - 10957L) * SECS_PER_DAY; + year -= 370; // == 1600; + } + + seconds += utc_offset; + + // Handle years in chunks of 400/100/4/1 + year += 400 * (seconds / SECS_PER_400_YEARS); + seconds %= SECS_PER_400_YEARS; + if (seconds < 0) + { + seconds += SECS_PER_400_YEARS; + year -= 400; + } + + leap_year = 1; // 4-century aligned + + sec_per_100years = SECS_PER_100_YEARS[leap_year]; + + while (seconds >= sec_per_100years) + { + seconds -= sec_per_100years; + year += 100; + leap_year = 0; // 1-century, non 4-century aligned + sec_per_100years = SECS_PER_100_YEARS[leap_year]; + } + + sec_per_4years = SECS_PER_4_YEARS[leap_year]; + while (seconds >= sec_per_4years) + { + seconds -= sec_per_4years; + year += 4; + leap_year = 1; // 4-year, non century aligned + sec_per_4years = SECS_PER_4_YEARS[leap_year]; + } + + sec_per_year = SECS_PER_YEAR[leap_year]; + while (seconds >= sec_per_year) + { + seconds -= sec_per_year; + year += 1; + leap_year = 0; // non 4-year aligned + sec_per_year = SECS_PER_YEAR[leap_year]; + } + + // Handle months and days + month = TM_DECEMBER + 1; + day = seconds / SECS_PER_DAY + 1; + seconds %= SECS_PER_DAY; + while (month != TM_JANUARY + 1) + { + month_offset = MONTHS_OFFSETS[leap_year][month]; + if (day > month_offset) + { + day -= month_offset; + break; + } + + month -= 1; + } + + // Handle hours, minutes and seconds + hour = seconds / SECS_PER_HOUR; + seconds %= SECS_PER_HOUR; + minute = seconds / SECS_PER_MIN; + second = seconds % SECS_PER_MIN; + + return Py_BuildValue("NNNNNNN", + PyLong_FromLong(year), + PyLong_FromLong(month), + PyLong_FromLong(day), + PyLong_FromLong(hour), + PyLong_FromLong(minute), + PyLong_FromLong(second), + PyLong_FromLong(microsecond)); +} + +// Calculate a precise difference between two datetimes. +PyObject *precise_diff(PyObject *self, PyObject *args) +{ + PyObject *dt1; + PyObject *dt2; + + if (!PyArg_ParseTuple(args, "OO", &dt1, &dt2)) + { + PyErr_SetString( + PyExc_ValueError, "Invalid parameters"); + return NULL; + } + + int year_diff = 0; + int month_diff = 0; + int day_diff = 0; + int hour_diff = 0; + int minute_diff = 0; + int second_diff = 0; + int microsecond_diff = 0; + int sign = 1; + int year; + int month; + int leap; + int days_in_last_month; + int days_in_month; + int dt1_year = PyDateTime_GET_YEAR(dt1); + int dt2_year = PyDateTime_GET_YEAR(dt2); + int dt1_month = PyDateTime_GET_MONTH(dt1); + int dt2_month = PyDateTime_GET_MONTH(dt2); + int dt1_day = PyDateTime_GET_DAY(dt1); + int dt2_day = PyDateTime_GET_DAY(dt2); + int dt1_hour = 0; + int dt2_hour = 0; + int dt1_minute = 0; + int dt2_minute = 0; + int dt1_second = 0; + int dt2_second = 0; + int dt1_microsecond = 0; + int dt2_microsecond = 0; + int dt1_total_seconds = 0; + int dt2_total_seconds = 0; + int dt1_offset = 0; + int dt2_offset = 0; + int dt1_is_datetime = PyDateTime_Check(dt1); + int dt2_is_datetime = PyDateTime_Check(dt2); + char *tz1 = ""; + char *tz2 = ""; + int in_same_tz = 0; + int total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day)); + + // If both dates are datetimes, we check + // If we are in the same timezone + if (dt1_is_datetime && dt2_is_datetime) + { + if (_has_tzinfo(dt1)) + { + tz1 = _get_tz_name(dt1); + dt1_offset = _get_offset(dt1); + } + + if (_has_tzinfo(dt2)) + { + tz2 = _get_tz_name(dt2); + dt2_offset = _get_offset(dt2); + } + + in_same_tz = tz1 == tz2 && strncmp(tz1, "", 1); + } + + // If we have datetimes (and not only dates) + // we get the information we need + if (dt1_is_datetime) + { + dt1_hour = PyDateTime_DATE_GET_HOUR(dt1); + dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1); + dt1_second = PyDateTime_DATE_GET_SECOND(dt1); + dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1); + + if ((!in_same_tz && dt1_offset != 0) || total_days == 0) + { + dt1_hour -= dt1_offset / SECS_PER_HOUR; + dt1_offset %= SECS_PER_HOUR; + dt1_minute -= dt1_offset / SECS_PER_MIN; + dt1_offset %= SECS_PER_MIN; + dt1_second -= dt1_offset; + + if (dt1_second < 0) + { + dt1_second += 60; + dt1_minute -= 1; + } + else if (dt1_second > 60) + { + dt1_second -= 60; + dt1_minute += 1; + } + + if (dt1_minute < 0) + { + dt1_minute += 60; + dt1_hour -= 1; + } + else if (dt1_minute > 60) + { + dt1_minute -= 60; + dt1_hour += 1; + } + + if (dt1_hour < 0) + { + dt1_hour += 24; + dt1_day -= 1; + } + else if (dt1_hour > 24) + { + dt1_hour -= 24; + dt1_day += 1; + } + } + + dt1_total_seconds = (dt1_hour * SECS_PER_HOUR + dt1_minute * SECS_PER_MIN + dt1_second); + } + + if (dt2_is_datetime) + { + dt2_hour = PyDateTime_DATE_GET_HOUR(dt2); + dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2); + dt2_second = PyDateTime_DATE_GET_SECOND(dt2); + dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2); + + if ((!in_same_tz && dt2_offset != 0) || total_days == 0) + { + dt2_hour -= dt2_offset / SECS_PER_HOUR; + dt2_offset %= SECS_PER_HOUR; + dt2_minute -= dt2_offset / SECS_PER_MIN; + dt2_offset %= SECS_PER_MIN; + dt2_second -= dt2_offset; + + if (dt2_second < 0) + { + dt2_second += 60; + dt2_minute -= 1; + } + else if (dt2_second > 60) + { + dt2_second -= 60; + dt2_minute += 1; + } + + if (dt2_minute < 0) + { + dt2_minute += 60; + dt2_hour -= 1; + } + else if (dt2_minute > 60) + { + dt2_minute -= 60; + dt2_hour += 1; + } + + if (dt2_hour < 0) + { + dt2_hour += 24; + dt2_day -= 1; + } + else if (dt2_hour > 24) + { + dt2_hour -= 24; + dt2_day += 1; + } + } + + dt2_total_seconds = (dt2_hour * SECS_PER_HOUR + dt2_minute * SECS_PER_MIN + dt2_second); + } + + // Direct comparison between two datetimes does not work + // so we need to check by properties + int dt1_gt_dt2 = (dt1_year > dt2_year || (dt1_year == dt2_year && dt1_month > dt2_month) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day > dt2_day) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds > dt2_total_seconds) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds == dt2_total_seconds && dt1_microsecond > dt2_microsecond)); + + if (dt1_gt_dt2) + { + PyObject *temp; + temp = dt1; + dt1 = dt2; + dt2 = temp; + sign = -1; + + // Retrieving properties + dt1_year = PyDateTime_GET_YEAR(dt1); + dt2_year = PyDateTime_GET_YEAR(dt2); + dt1_month = PyDateTime_GET_MONTH(dt1); + dt2_month = PyDateTime_GET_MONTH(dt2); + dt1_day = PyDateTime_GET_DAY(dt1); + dt2_day = PyDateTime_GET_DAY(dt2); + + if (dt2_is_datetime) + { + dt1_hour = PyDateTime_DATE_GET_HOUR(dt1); + dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1); + dt1_second = PyDateTime_DATE_GET_SECOND(dt1); + dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1); + } + + if (dt1_is_datetime) + { + dt2_hour = PyDateTime_DATE_GET_HOUR(dt2); + dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2); + dt2_second = PyDateTime_DATE_GET_SECOND(dt2); + dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2); + } + + total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day)); + } + + year_diff = dt2_year - dt1_year; + month_diff = dt2_month - dt1_month; + day_diff = dt2_day - dt1_day; + hour_diff = dt2_hour - dt1_hour; + minute_diff = dt2_minute - dt1_minute; + second_diff = dt2_second - dt1_second; + microsecond_diff = dt2_microsecond - dt1_microsecond; + + if (microsecond_diff < 0) + { + microsecond_diff += 1e6; + second_diff -= 1; + } + + if (second_diff < 0) + { + second_diff += 60; + minute_diff -= 1; + } + + if (minute_diff < 0) + { + minute_diff += 60; + hour_diff -= 1; + } + + if (hour_diff < 0) + { + hour_diff += 24; + day_diff -= 1; + } + + if (day_diff < 0) + { + // If we have a difference in days, + // we have to check if they represent months + year = dt2_year; + month = dt2_month; + + if (month == 1) + { + month = 12; + year -= 1; + } + else + { + month -= 1; + } + + leap = _is_leap(year); + + days_in_last_month = DAYS_PER_MONTHS[leap][month]; + days_in_month = DAYS_PER_MONTHS[_is_leap(dt2_year)][dt2_month]; + + if (day_diff < days_in_month - days_in_last_month) + { + // We don't have a full month, we calculate days + if (days_in_last_month < dt1_day) + { + day_diff += dt1_day; + } + else + { + day_diff += days_in_last_month; + } + } + else if (day_diff == days_in_month - days_in_last_month) + { + // We have exactly a full month + // We remove the days difference + // and add one to the months difference + day_diff = 0; + month_diff += 1; + } + else + { + // We have a full month + day_diff += days_in_last_month; + } + + month_diff -= 1; + } + + if (month_diff < 0) + { + month_diff += 12; + year_diff -= 1; + } + + return new_diff( + year_diff * sign, + month_diff * sign, + day_diff * sign, + hour_diff * sign, + minute_diff * sign, + second_diff * sign, + microsecond_diff * sign, + total_days * sign); +} + +/* ------------------------------------------------------------------------- */ + +static PyMethodDef helpers_methods[] = { + {"is_leap", + (PyCFunction)is_leap, + METH_VARARGS, + PyDoc_STR("Checks if a year is a leap year.")}, + {"is_long_year", + (PyCFunction)is_long_year, + METH_VARARGS, + PyDoc_STR("Checks if a year is a long year.")}, + {"week_day", + (PyCFunction)week_day, + METH_VARARGS, + PyDoc_STR("Returns the weekday number.")}, + {"days_in_year", + (PyCFunction)days_in_year, + METH_VARARGS, + PyDoc_STR("Returns the number of days in the given year.")}, + {"timestamp", + (PyCFunction)timestamp, + METH_VARARGS, + PyDoc_STR("Returns the timestamp of the given datetime.")}, + {"local_time", + (PyCFunction)local_time, + METH_VARARGS, + PyDoc_STR("Returns a UNIX time as a broken down time for a particular transition type.")}, + {"precise_diff", + (PyCFunction)precise_diff, + METH_VARARGS, + PyDoc_STR("Calculate a precise difference between two datetimes.")}, + {NULL}}; + +/* ------------------------------------------------------------------------- */ + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_helpers", + NULL, + -1, + helpers_methods, + NULL, + NULL, + NULL, + NULL, +}; + +PyMODINIT_FUNC +PyInit__helpers(void) +{ + PyObject *module; + + PyDateTime_IMPORT; + + module = PyModule_Create(&moduledef); + + if (module == NULL) + return NULL; + + // Diff declaration + Diff_type.tp_new = PyType_GenericNew; + Diff_type.tp_members = Diff_members; + Diff_type.tp_init = (initproc)Diff_init; + + if (PyType_Ready(&Diff_type) < 0) + return NULL; + + PyModule_AddObject(module, "PreciseDiff", (PyObject *)&Diff_type); + + return module; +} diff --git a/pendulum/_extensions/helpers.py b/pendulum/_extensions/helpers.py new file mode 100644 index 0000000..0132c0c --- /dev/null +++ b/pendulum/_extensions/helpers.py @@ -0,0 +1,358 @@ +import datetime +import math +import typing + +from collections import namedtuple + +from ..constants import DAY_OF_WEEK_TABLE +from ..constants import DAYS_PER_L_YEAR +from ..constants import DAYS_PER_MONTHS +from ..constants import DAYS_PER_N_YEAR +from ..constants import EPOCH_YEAR +from ..constants import MONTHS_OFFSETS +from ..constants import SECS_PER_4_YEARS +from ..constants import SECS_PER_100_YEARS +from ..constants import SECS_PER_400_YEARS +from ..constants import SECS_PER_DAY +from ..constants import SECS_PER_HOUR +from ..constants import SECS_PER_MIN +from ..constants import SECS_PER_YEAR +from ..constants import TM_DECEMBER +from ..constants import TM_JANUARY + + +class PreciseDiff( + namedtuple( + "PreciseDiff", + "years months days " "hours minutes seconds microseconds " "total_days", + ) +): + def __repr__(self): + return ( + "{years} years " + "{months} months " + "{days} days " + "{hours} hours " + "{minutes} minutes " + "{seconds} seconds " + "{microseconds} microseconds" + ).format( + years=self.years, + months=self.months, + days=self.days, + hours=self.hours, + minutes=self.minutes, + seconds=self.seconds, + microseconds=self.microseconds, + ) + + +def is_leap(year): # type: (int) -> bool + return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) + + +def is_long_year(year): # type: (int) -> bool + def p(y): + return y + y // 4 - y // 100 + y // 400 + + return p(year) % 7 == 4 or p(year - 1) % 7 == 3 + + +def week_day(year, month, day): # type: (int, int, int) -> int + if month < 3: + year -= 1 + + w = ( + year + + year // 4 + - year // 100 + + year // 400 + + DAY_OF_WEEK_TABLE[month - 1] + + day + ) % 7 + + if not w: + w = 7 + + return w + + +def days_in_year(year): # type: (int) -> int + if is_leap(year): + return DAYS_PER_L_YEAR + + return DAYS_PER_N_YEAR + + +def timestamp(dt): # type: (datetime.datetime) -> int + year = dt.year + + result = (year - 1970) * 365 + MONTHS_OFFSETS[0][dt.month] + result += (year - 1968) // 4 + result -= (year - 1900) // 100 + result += (year - 1600) // 400 + + if is_leap(year) and dt.month < 3: + result -= 1 + + result += dt.day - 1 + result *= 24 + result += dt.hour + result *= 60 + result += dt.minute + result *= 60 + result += dt.second + + return result + + +def local_time( + unix_time, utc_offset, microseconds +): # type: (int, int, int) -> typing.Tuple[int, int, int, int, int, int, int] + """ + Returns a UNIX time as a broken down time + for a particular transition type. + + :type unix_time: int + :type utc_offset: int + :type microseconds: int + + :rtype: tuple + """ + year = EPOCH_YEAR + seconds = int(math.floor(unix_time)) + + # Shift to a base year that is 400-year aligned. + if seconds >= 0: + seconds -= 10957 * SECS_PER_DAY + year += 30 # == 2000 + else: + seconds += (146097 - 10957) * SECS_PER_DAY + year -= 370 # == 1600 + + seconds += utc_offset + + # Handle years in chunks of 400/100/4/1 + year += 400 * (seconds // SECS_PER_400_YEARS) + seconds %= SECS_PER_400_YEARS + if seconds < 0: + seconds += SECS_PER_400_YEARS + year -= 400 + + leap_year = 1 # 4-century aligned + + sec_per_100years = SECS_PER_100_YEARS[leap_year] + while seconds >= sec_per_100years: + seconds -= sec_per_100years + year += 100 + leap_year = 0 # 1-century, non 4-century aligned + sec_per_100years = SECS_PER_100_YEARS[leap_year] + + sec_per_4years = SECS_PER_4_YEARS[leap_year] + while seconds >= sec_per_4years: + seconds -= sec_per_4years + year += 4 + leap_year = 1 # 4-year, non century aligned + sec_per_4years = SECS_PER_4_YEARS[leap_year] + + sec_per_year = SECS_PER_YEAR[leap_year] + while seconds >= sec_per_year: + seconds -= sec_per_year + year += 1 + leap_year = 0 # non 4-year aligned + sec_per_year = SECS_PER_YEAR[leap_year] + + # Handle months and days + month = TM_DECEMBER + 1 + day = seconds // SECS_PER_DAY + 1 + seconds %= SECS_PER_DAY + while month != TM_JANUARY + 1: + month_offset = MONTHS_OFFSETS[leap_year][month] + if day > month_offset: + day -= month_offset + break + + month -= 1 + + # Handle hours, minutes, seconds and microseconds + hour = seconds // SECS_PER_HOUR + seconds %= SECS_PER_HOUR + minute = seconds // SECS_PER_MIN + second = seconds % SECS_PER_MIN + + return (year, month, day, hour, minute, second, microseconds) + + +def precise_diff( + d1, d2 +): # type: (typing.Union[datetime.datetime, datetime.date], typing.Union[datetime.datetime, datetime.date]) -> PreciseDiff + """ + Calculate a precise difference between two datetimes. + + :param d1: The first datetime + :type d1: datetime.datetime or datetime.date + + :param d2: The second datetime + :type d2: datetime.datetime or datetime.date + + :rtype: PreciseDiff + """ + sign = 1 + + if d1 == d2: + return PreciseDiff(0, 0, 0, 0, 0, 0, 0, 0) + + tzinfo1 = d1.tzinfo if isinstance(d1, datetime.datetime) else None + tzinfo2 = d2.tzinfo if isinstance(d2, datetime.datetime) else None + + if ( + tzinfo1 is None + and tzinfo2 is not None + or tzinfo2 is None + and tzinfo1 is not None + ): + raise ValueError( + "Comparison between naive and aware datetimes is not supported" + ) + + if d1 > d2: + d1, d2 = d2, d1 + sign = -1 + + d_diff = 0 + hour_diff = 0 + min_diff = 0 + sec_diff = 0 + mic_diff = 0 + total_days = _day_number(d2.year, d2.month, d2.day) - _day_number( + d1.year, d1.month, d1.day + ) + in_same_tz = False + tz1 = None + tz2 = None + + # Trying to figure out the timezone names + # If we can't find them, we assume different timezones + if tzinfo1 and tzinfo2: + if hasattr(tzinfo1, "name"): + # Pendulum timezone + tz1 = tzinfo1.name + elif hasattr(tzinfo1, "zone"): + # pytz timezone + tz1 = tzinfo1.zone + + if hasattr(tzinfo2, "name"): + tz2 = tzinfo2.name + elif hasattr(tzinfo2, "zone"): + tz2 = tzinfo2.zone + + in_same_tz = tz1 == tz2 and tz1 is not None + + if isinstance(d2, datetime.datetime): + if isinstance(d1, datetime.datetime): + # If we are not in the same timezone + # we need to adjust + # + # We also need to adjust if we do not + # have variable-length units + if not in_same_tz or total_days == 0: + offset1 = d1.utcoffset() + offset2 = d2.utcoffset() + + if offset1: + d1 = d1 - offset1 + + if offset2: + d2 = d2 - offset2 + + hour_diff = d2.hour - d1.hour + min_diff = d2.minute - d1.minute + sec_diff = d2.second - d1.second + mic_diff = d2.microsecond - d1.microsecond + else: + hour_diff = d2.hour + min_diff = d2.minute + sec_diff = d2.second + mic_diff = d2.microsecond + + if mic_diff < 0: + mic_diff += 1000000 + sec_diff -= 1 + + if sec_diff < 0: + sec_diff += 60 + min_diff -= 1 + + if min_diff < 0: + min_diff += 60 + hour_diff -= 1 + + if hour_diff < 0: + hour_diff += 24 + d_diff -= 1 + + y_diff = d2.year - d1.year + m_diff = d2.month - d1.month + d_diff += d2.day - d1.day + + if d_diff < 0: + year = d2.year + month = d2.month + + if month == 1: + month = 12 + year -= 1 + else: + month -= 1 + + leap = int(is_leap(year)) + + days_in_last_month = DAYS_PER_MONTHS[leap][month] + days_in_month = DAYS_PER_MONTHS[int(is_leap(d2.year))][d2.month] + + if d_diff < days_in_month - days_in_last_month: + # We don't have a full month, we calculate days + if days_in_last_month < d1.day: + d_diff += d1.day + else: + d_diff += days_in_last_month + elif d_diff == days_in_month - days_in_last_month: + # We have exactly a full month + # We remove the days difference + # and add one to the months difference + d_diff = 0 + m_diff += 1 + else: + # We have a full month + d_diff += days_in_last_month + + m_diff -= 1 + + if m_diff < 0: + m_diff += 12 + y_diff -= 1 + + return PreciseDiff( + sign * y_diff, + sign * m_diff, + sign * d_diff, + sign * hour_diff, + sign * min_diff, + sign * sec_diff, + sign * mic_diff, + sign * total_days, + ) + + +def _day_number(year, month, day): # type: (int, int, int) -> int + month = (month + 9) % 12 + year = year - month // 10 + + return ( + 365 * year + + year // 4 + - year // 100 + + year // 400 + + (month * 306 + 5) // 10 + + (day - 1) + ) diff --git a/pendulum/constants.py b/pendulum/constants.py new file mode 100644 index 0000000..38697d7 --- /dev/null +++ b/pendulum/constants.py @@ -0,0 +1,107 @@ +# The day constants +SUNDAY = 0 +MONDAY = 1 +TUESDAY = 2 +WEDNESDAY = 3 +THURSDAY = 4 +FRIDAY = 5 +SATURDAY = 6 + +# Number of X in Y. +YEARS_PER_CENTURY = 100 +YEARS_PER_DECADE = 10 +MONTHS_PER_YEAR = 12 +WEEKS_PER_YEAR = 52 +DAYS_PER_WEEK = 7 +HOURS_PER_DAY = 24 +MINUTES_PER_HOUR = 60 +SECONDS_PER_MINUTE = 60 +SECONDS_PER_HOUR = MINUTES_PER_HOUR * SECONDS_PER_MINUTE +SECONDS_PER_DAY = HOURS_PER_DAY * SECONDS_PER_HOUR +US_PER_SECOND = 1000000 + +# Formats +ATOM = "YYYY-MM-DDTHH:mm:ssZ" +COOKIE = "dddd, DD-MMM-YYYY HH:mm:ss zz" +ISO8601 = "YYYY-MM-DDTHH:mm:ssZ" +ISO8601_EXTENDED = "YYYY-MM-DDTHH:mm:ss.SSSSSSZ" +RFC822 = "ddd, DD MMM YY HH:mm:ss ZZ" +RFC850 = "dddd, DD-MMM-YY HH:mm:ss zz" +RFC1036 = "ddd, DD MMM YY HH:mm:ss ZZ" +RFC1123 = "ddd, DD MMM YYYY HH:mm:ss ZZ" +RFC2822 = "ddd, DD MMM YYYY HH:mm:ss ZZ" +RFC3339 = ISO8601 +RFC3339_EXTENDED = ISO8601_EXTENDED +RSS = "ddd, DD MMM YYYY HH:mm:ss ZZ" +W3C = ISO8601 + + +EPOCH_YEAR = 1970 + +DAYS_PER_N_YEAR = 365 +DAYS_PER_L_YEAR = 366 + +USECS_PER_SEC = 1000000 + +SECS_PER_MIN = 60 +SECS_PER_HOUR = 60 * SECS_PER_MIN +SECS_PER_DAY = SECS_PER_HOUR * 24 + +# 400-year chunks always have 146097 days (20871 weeks). +SECS_PER_400_YEARS = 146097 * SECS_PER_DAY + +# The number of seconds in an aligned 100-year chunk, for those that +# do not begin with a leap year and those that do respectively. +SECS_PER_100_YEARS = ( + (76 * DAYS_PER_N_YEAR + 24 * DAYS_PER_L_YEAR) * SECS_PER_DAY, + (75 * DAYS_PER_N_YEAR + 25 * DAYS_PER_L_YEAR) * SECS_PER_DAY, +) + +# The number of seconds in an aligned 4-year chunk, for those that +# do not begin with a leap year and those that do respectively. +SECS_PER_4_YEARS = ( + (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY, + (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY, +) + +# The number of seconds in non-leap and leap years respectively. +SECS_PER_YEAR = (DAYS_PER_N_YEAR * SECS_PER_DAY, DAYS_PER_L_YEAR * SECS_PER_DAY) + +DAYS_PER_YEAR = (DAYS_PER_N_YEAR, DAYS_PER_L_YEAR) + +# The month lengths in non-leap and leap years respectively. +DAYS_PER_MONTHS = ( + (-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31), + (-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31), +) + +# The day offsets of the beginning of each (1-based) month in non-leap +# and leap years respectively. +# For example, in a leap year there are 335 days before December. +MONTHS_OFFSETS = ( + (-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365), + (-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366), +) + +DAY_OF_WEEK_TABLE = (0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4) + +TM_SUNDAY = 0 +TM_MONDAY = 1 +TM_TUESDAY = 2 +TM_WEDNESDAY = 3 +TM_THURSDAY = 4 +TM_FRIDAY = 5 +TM_SATURDAY = 6 + +TM_JANUARY = 0 +TM_FEBRUARY = 1 +TM_MARCH = 2 +TM_APRIL = 3 +TM_MAY = 4 +TM_JUNE = 5 +TM_JULY = 6 +TM_AUGUST = 7 +TM_SEPTEMBER = 8 +TM_OCTOBER = 9 +TM_NOVEMBER = 10 +TM_DECEMBER = 11 diff --git a/pendulum/date.py b/pendulum/date.py new file mode 100644 index 0000000..f11164e --- /dev/null +++ b/pendulum/date.py @@ -0,0 +1,891 @@ +from __future__ import absolute_import +from __future__ import division + +import calendar +import math + +from datetime import date +from datetime import timedelta + +import pendulum + +from .constants import FRIDAY +from .constants import MONDAY +from .constants import MONTHS_PER_YEAR +from .constants import SATURDAY +from .constants import SUNDAY +from .constants import THURSDAY +from .constants import TUESDAY +from .constants import WEDNESDAY +from .constants import YEARS_PER_CENTURY +from .constants import YEARS_PER_DECADE +from .exceptions import PendulumException +from .helpers import add_duration +from .mixins.default import FormattableMixin +from .period import Period + + +class Date(FormattableMixin, date): + + # Names of days of the week + _days = { + SUNDAY: "Sunday", + MONDAY: "Monday", + TUESDAY: "Tuesday", + WEDNESDAY: "Wednesday", + THURSDAY: "Thursday", + FRIDAY: "Friday", + SATURDAY: "Saturday", + } + + _MODIFIERS_VALID_UNITS = ["day", "week", "month", "year", "decade", "century"] + + # Getters/Setters + + def set(self, year=None, month=None, day=None): + return self.replace(year=year, month=month, day=day) + + @property + def day_of_week(self): + """ + Returns the day of the week (0-6). + + :rtype: int + """ + return self.isoweekday() % 7 + + @property + def day_of_year(self): + """ + Returns the day of the year (1-366). + + :rtype: int + """ + k = 1 if self.is_leap_year() else 2 + + return (275 * self.month) // 9 - k * ((self.month + 9) // 12) + self.day - 30 + + @property + def week_of_year(self): + return self.isocalendar()[1] + + @property + def days_in_month(self): + return calendar.monthrange(self.year, self.month)[1] + + @property + def week_of_month(self): + first_day_of_month = self.replace(day=1) + + return self.week_of_year - first_day_of_month.week_of_year + 1 + + @property + def age(self): + return self.diff(abs=False).in_years() + + @property + def quarter(self): + return int(math.ceil(self.month / 3)) + + # String Formatting + + def to_date_string(self): + """ + Format the instance as date. + + :rtype: str + """ + return self.strftime("%Y-%m-%d") + + def to_formatted_date_string(self): + """ + Format the instance as a readable date. + + :rtype: str + """ + return self.strftime("%b %d, %Y") + + def __repr__(self): + return ( + "{klass}(" + "{year}, {month}, {day}" + ")".format( + klass=self.__class__.__name__, + year=self.year, + month=self.month, + day=self.day, + ) + ) + + # COMPARISONS + + def closest(self, dt1, dt2): + """ + Get the closest date from the instance. + + :type dt1: Date or date + :type dt2: Date or date + + :rtype: Date + """ + dt1 = self.__class__(dt1.year, dt1.month, dt1.day) + dt2 = self.__class__(dt2.year, dt2.month, dt2.day) + + if self.diff(dt1).in_seconds() < self.diff(dt2).in_seconds(): + return dt1 + + return dt2 + + def farthest(self, dt1, dt2): + """ + Get the farthest date from the instance. + + :type dt1: Date or date + :type dt2: Date or date + + :rtype: Date + """ + dt1 = self.__class__(dt1.year, dt1.month, dt1.day) + dt2 = self.__class__(dt2.year, dt2.month, dt2.day) + + if self.diff(dt1).in_seconds() > self.diff(dt2).in_seconds(): + return dt1 + + return dt2 + + def is_future(self): + """ + Determines if the instance is in the future, ie. greater than now. + + :rtype: bool + """ + return self > self.today() + + def is_past(self): + """ + Determines if the instance is in the past, ie. less than now. + + :rtype: bool + """ + return self < self.today() + + def is_leap_year(self): + """ + Determines if the instance is a leap year. + + :rtype: bool + """ + return calendar.isleap(self.year) + + def is_long_year(self): + """ + Determines if the instance is a long year + + See link `<https://en.wikipedia.org/wiki/ISO_8601#Week_dates>`_ + + :rtype: bool + """ + return Date(self.year, 12, 28).isocalendar()[1] == 53 + + def is_same_day(self, dt): + """ + Checks if the passed in date is the same day as the instance current day. + + :type dt: Date or date + + :rtype: bool + """ + return self == dt + + def is_anniversary(self, dt=None): + """ + Check if its the anniversary. + + Compares the date/month values of the two dates. + + :rtype: bool + """ + if dt is None: + dt = Date.today() + + instance = self.__class__(dt.year, dt.month, dt.day) + + return (self.month, self.day) == (instance.month, instance.day) + + # the additional method for checking if today is the anniversary day + # the alias is provided to start using a new name and keep the backward compatibility + # the old name can be completely replaced with the new in one of the future versions + is_birthday = is_anniversary + + # ADDITIONS AND SUBSTRACTIONS + + def add(self, years=0, months=0, weeks=0, days=0): + """ + Add duration to the instance. + + :param years: The number of years + :type years: int + + :param months: The number of months + :type months: int + + :param weeks: The number of weeks + :type weeks: int + + :param days: The number of days + :type days: int + + :rtype: Date + """ + dt = add_duration( + date(self.year, self.month, self.day), + years=years, + months=months, + weeks=weeks, + days=days, + ) + + return self.__class__(dt.year, dt.month, dt.day) + + def subtract(self, years=0, months=0, weeks=0, days=0): + """ + Remove duration from the instance. + + :param years: The number of years + :type years: int + + :param months: The number of months + :type months: int + + :param weeks: The number of weeks + :type weeks: int + + :param days: The number of days + :type days: int + + :rtype: Date + """ + return self.add(years=-years, months=-months, weeks=-weeks, days=-days) + + def _add_timedelta(self, delta): + """ + Add timedelta duration to the instance. + + :param delta: The timedelta instance + :type delta: pendulum.Duration or datetime.timedelta + + :rtype: Date + """ + if isinstance(delta, pendulum.Duration): + return self.add( + years=delta.years, + months=delta.months, + weeks=delta.weeks, + days=delta.remaining_days, + ) + + return self.add(days=delta.days) + + def _subtract_timedelta(self, delta): + """ + Remove timedelta duration from the instance. + + :param delta: The timedelta instance + :type delta: pendulum.Duration or datetime.timedelta + + :rtype: Date + """ + if isinstance(delta, pendulum.Duration): + return self.subtract( + years=delta.years, + months=delta.months, + weeks=delta.weeks, + days=delta.remaining_days, + ) + + return self.subtract(days=delta.days) + + def __add__(self, other): + if not isinstance(other, timedelta): + return NotImplemented + + return self._add_timedelta(other) + + def __sub__(self, other): + if isinstance(other, timedelta): + return self._subtract_timedelta(other) + + if not isinstance(other, date): + return NotImplemented + + dt = self.__class__(other.year, other.month, other.day) + + return dt.diff(self, False) + + # DIFFERENCES + + def diff(self, dt=None, abs=True): + """ + Returns the difference between two Date objects as a Period. + + :type dt: Date or None + + :param abs: Whether to return an absolute interval or not + :type abs: bool + + :rtype: Period + """ + if dt is None: + dt = self.today() + + return Period(self, Date(dt.year, dt.month, dt.day), absolute=abs) + + def diff_for_humans(self, other=None, absolute=False, locale=None): + """ + Get the difference in a human readable format in the current locale. + + When comparing a value in the past to default now: + 1 day ago + 5 months ago + + When comparing a value in the future to default now: + 1 day from now + 5 months from now + + When comparing a value in the past to another value: + 1 day before + 5 months before + + When comparing a value in the future to another value: + 1 day after + 5 months after + + :type other: Date + + :param absolute: removes time difference modifiers ago, after, etc + :type absolute: bool + + :param locale: The locale to use for localization + :type locale: str + + :rtype: str + """ + is_now = other is None + + if is_now: + other = self.today() + + diff = self.diff(other) + + return pendulum.format_diff(diff, is_now, absolute, locale) + + # MODIFIERS + + def start_of(self, unit): + """ + Returns a copy of the instance with the time reset + with the following rules: + + * day: time to 00:00:00 + * week: date to first day of the week and time to 00:00:00 + * month: date to first day of the month and time to 00:00:00 + * year: date to first day of the year and time to 00:00:00 + * decade: date to first day of the decade and time to 00:00:00 + * century: date to first day of century and time to 00:00:00 + + :param unit: The unit to reset to + :type unit: str + + :rtype: Date + """ + if unit not in self._MODIFIERS_VALID_UNITS: + raise ValueError('Invalid unit "{}" for start_of()'.format(unit)) + + return getattr(self, "_start_of_{}".format(unit))() + + def end_of(self, unit): + """ + Returns a copy of the instance with the time reset + with the following rules: + + * week: date to last day of the week + * month: date to last day of the month + * year: date to last day of the year + * decade: date to last day of the decade + * century: date to last day of century + + :param unit: The unit to reset to + :type unit: str + + :rtype: Date + """ + if unit not in self._MODIFIERS_VALID_UNITS: + raise ValueError('Invalid unit "%s" for end_of()' % unit) + + return getattr(self, "_end_of_%s" % unit)() + + def _start_of_day(self): + """ + Compatibility method. + + :rtype: Date + """ + return self + + def _end_of_day(self): + """ + Compatibility method + + :rtype: Date + """ + return self + + def _start_of_month(self): + """ + Reset the date to the first day of the month. + + :rtype: Date + """ + return self.set(self.year, self.month, 1) + + def _end_of_month(self): + """ + Reset the date to the last day of the month. + + :rtype: Date + """ + return self.set(self.year, self.month, self.days_in_month) + + def _start_of_year(self): + """ + Reset the date to the first day of the year. + + :rtype: Date + """ + return self.set(self.year, 1, 1) + + def _end_of_year(self): + """ + Reset the date to the last day of the year. + + :rtype: Date + """ + return self.set(self.year, 12, 31) + + def _start_of_decade(self): + """ + Reset the date to the first day of the decade. + + :rtype: Date + """ + year = self.year - self.year % YEARS_PER_DECADE + + return self.set(year, 1, 1) + + def _end_of_decade(self): + """ + Reset the date to the last day of the decade. + + :rtype: Date + """ + year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1 + + return self.set(year, 12, 31) + + def _start_of_century(self): + """ + Reset the date to the first day of the century. + + :rtype: Date + """ + year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1 + + return self.set(year, 1, 1) + + def _end_of_century(self): + """ + Reset the date to the last day of the century. + + :rtype: Date + """ + year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY + + return self.set(year, 12, 31) + + def _start_of_week(self): + """ + Reset the date to the first day of the week. + + :rtype: Date + """ + dt = self + + if self.day_of_week != pendulum._WEEK_STARTS_AT: + dt = self.previous(pendulum._WEEK_STARTS_AT) + + return dt.start_of("day") + + def _end_of_week(self): + """ + Reset the date to the last day of the week. + + :rtype: Date + """ + dt = self + + if self.day_of_week != pendulum._WEEK_ENDS_AT: + dt = self.next(pendulum._WEEK_ENDS_AT) + + return dt.end_of("day") + + def next(self, day_of_week=None): + """ + Modify to the next occurrence of a given day of the week. + If no day_of_week is provided, modify to the next occurrence + of the current day of the week. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + :param day_of_week: The next day of week to reset to. + :type day_of_week: int or None + + :rtype: Date + """ + if day_of_week is None: + day_of_week = self.day_of_week + + if day_of_week < SUNDAY or day_of_week > SATURDAY: + raise ValueError("Invalid day of week") + + dt = self.add(days=1) + while dt.day_of_week != day_of_week: + dt = dt.add(days=1) + + return dt + + def previous(self, day_of_week=None): + """ + Modify to the previous occurrence of a given day of the week. + If no day_of_week is provided, modify to the previous occurrence + of the current day of the week. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + :param day_of_week: The previous day of week to reset to. + :type day_of_week: int or None + + :rtype: Date + """ + if day_of_week is None: + day_of_week = self.day_of_week + + if day_of_week < SUNDAY or day_of_week > SATURDAY: + raise ValueError("Invalid day of week") + + dt = self.subtract(days=1) + while dt.day_of_week != day_of_week: + dt = dt.subtract(days=1) + + return dt + + def first_of(self, unit, day_of_week=None): + """ + Returns an instance set to the first occurrence + of a given day of the week in the current unit. + If no day_of_week is provided, modify to the first day of the unit. + Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. + + Supported units are month, quarter and year. + + :param unit: The unit to use + :type unit: str + + :type day_of_week: int or None + + :rtype: Date + """ + if unit not in ["month", "quarter", "year"]: + raise ValueError('Invalid unit "{}" for first_of()'.format(unit)) + + return getattr(self, "_first_of_{}".format(unit))(day_of_week) + + def last_of(self, unit, day_of_week=None): + """ + Returns an instance set to the last occurrence + of a given day of the week in the current unit. + If no day_of_week is provided, modify to the last day of the unit. + Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. + + Supported units are month, quarter and year. + + :param unit: The unit to use + :type unit: str + + :type day_of_week: int or None + + :rtype: Date + """ + if unit not in ["month", "quarter", "year"]: + raise ValueError('Invalid unit "{}" for first_of()'.format(unit)) + + return getattr(self, "_last_of_{}".format(unit))(day_of_week) + + def nth_of(self, unit, nth, day_of_week): + """ + Returns a new instance set to the given occurrence + of a given day of the week in the current unit. + If the calculated occurrence is outside the scope of the current unit, + then raise an error. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + Supported units are month, quarter and year. + + :param unit: The unit to use + :type unit: str + + :type nth: int + + :type day_of_week: int or None + + :rtype: Date + """ + if unit not in ["month", "quarter", "year"]: + raise ValueError('Invalid unit "{}" for first_of()'.format(unit)) + + dt = getattr(self, "_nth_of_{}".format(unit))(nth, day_of_week) + if dt is False: + raise PendulumException( + "Unable to find occurence {} of {} in {}".format( + nth, self._days[day_of_week], unit + ) + ) + + return dt + + def _first_of_month(self, day_of_week): + """ + Modify to the first occurrence of a given day of the week + in the current month. If no day_of_week is provided, + modify to the first day of the month. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + :type day_of_week: int + + :rtype: Date + """ + dt = self + + if day_of_week is None: + return dt.set(day=1) + + month = calendar.monthcalendar(dt.year, dt.month) + + calendar_day = (day_of_week - 1) % 7 + + if month[0][calendar_day] > 0: + day_of_month = month[0][calendar_day] + else: + day_of_month = month[1][calendar_day] + + return dt.set(day=day_of_month) + + def _last_of_month(self, day_of_week=None): + """ + Modify to the last occurrence of a given day of the week + in the current month. If no day_of_week is provided, + modify to the last day of the month. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + :type day_of_week: int or None + + :rtype: Date + """ + dt = self + + if day_of_week is None: + return dt.set(day=self.days_in_month) + + month = calendar.monthcalendar(dt.year, dt.month) + + calendar_day = (day_of_week - 1) % 7 + + if month[-1][calendar_day] > 0: + day_of_month = month[-1][calendar_day] + else: + day_of_month = month[-2][calendar_day] + + return dt.set(day=day_of_month) + + def _nth_of_month(self, nth, day_of_week): + """ + Modify to the given occurrence of a given day of the week + in the current month. If the calculated occurrence is outside, + the scope of the current month, then return False and no + modifications are made. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + :type nth: int + + :type day_of_week: int or None + + :rtype: Date + """ + if nth == 1: + return self.first_of("month", day_of_week) + + dt = self.first_of("month") + check = dt.format("YYYY-MM") + for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + dt = dt.next(day_of_week) + + if dt.format("YYYY-MM") == check: + return self.set(day=dt.day) + + return False + + def _first_of_quarter(self, day_of_week=None): + """ + Modify to the first occurrence of a given day of the week + in the current quarter. If no day_of_week is provided, + modify to the first day of the quarter. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + :type day_of_week: int or None + + :rtype: Date + """ + return self.set(self.year, self.quarter * 3 - 2, 1).first_of( + "month", day_of_week + ) + + def _last_of_quarter(self, day_of_week=None): + """ + Modify to the last occurrence of a given day of the week + in the current quarter. If no day_of_week is provided, + modify to the last day of the quarter. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + :type day_of_week: int or None + + :rtype: Date + """ + return self.set(self.year, self.quarter * 3, 1).last_of("month", day_of_week) + + def _nth_of_quarter(self, nth, day_of_week): + """ + Modify to the given occurrence of a given day of the week + in the current quarter. If the calculated occurrence is outside, + the scope of the current quarter, then return False and no + modifications are made. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + :type nth: int + + :type day_of_week: int or None + + :rtype: Date + """ + if nth == 1: + return self.first_of("quarter", day_of_week) + + dt = self.replace(self.year, self.quarter * 3, 1) + last_month = dt.month + year = dt.year + dt = dt.first_of("quarter") + for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + dt = dt.next(day_of_week) + + if last_month < dt.month or year != dt.year: + return False + + return self.set(self.year, dt.month, dt.day) + + def _first_of_year(self, day_of_week=None): + """ + Modify to the first occurrence of a given day of the week + in the current year. If no day_of_week is provided, + modify to the first day of the year. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + :type day_of_week: int or None + + :rtype: Date + """ + return self.set(month=1).first_of("month", day_of_week) + + def _last_of_year(self, day_of_week=None): + """ + Modify to the last occurrence of a given day of the week + in the current year. If no day_of_week is provided, + modify to the last day of the year. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + :type day_of_week: int or None + + :rtype: Date + """ + return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week) + + def _nth_of_year(self, nth, day_of_week): + """ + Modify to the given occurrence of a given day of the week + in the current year. If the calculated occurrence is outside, + the scope of the current year, then return False and no + modifications are made. Use the supplied consts + to indicate the desired day_of_week, ex. pendulum.MONDAY. + + :type nth: int + + :type day_of_week: int or None + + :rtype: Date + """ + if nth == 1: + return self.first_of("year", day_of_week) + + dt = self.first_of("year") + year = dt.year + for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + dt = dt.next(day_of_week) + + if year != dt.year: + return False + + return self.set(self.year, dt.month, dt.day) + + def average(self, dt=None): + """ + Modify the current instance to the average + of a given instance (default now) and the current instance. + + :type dt: Date or date + + :rtype: Date + """ + if dt is None: + dt = Date.today() + + return self.add(days=int(self.diff(dt, False).in_days() / 2)) + + # Native methods override + + @classmethod + def today(cls): + return pendulum.today().date() + + @classmethod + def fromtimestamp(cls, t): + dt = super(Date, cls).fromtimestamp(t) + + return cls(dt.year, dt.month, dt.day) + + @classmethod + def fromordinal(cls, n): + dt = super(Date, cls).fromordinal(n) + + return cls(dt.year, dt.month, dt.day) + + def replace(self, year=None, month=None, day=None): + year = year if year is not None else self.year + month = month if month is not None else self.month + day = day if day is not None else self.day + + return self.__class__(year, month, day) diff --git a/pendulum/datetime.py b/pendulum/datetime.py new file mode 100644 index 0000000..feb140f --- /dev/null +++ b/pendulum/datetime.py @@ -0,0 +1,1563 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import division + +import calendar +import datetime + +from typing import Optional +from typing import TypeVar +from typing import Union + +import pendulum + +from .constants import ATOM +from .constants import COOKIE +from .constants import MINUTES_PER_HOUR +from .constants import MONTHS_PER_YEAR +from .constants import RFC822 +from .constants import RFC850 +from .constants import RFC1036 +from .constants import RFC1123 +from .constants import RFC2822 +from .constants import RSS +from .constants import SATURDAY +from .constants import SECONDS_PER_DAY +from .constants import SECONDS_PER_MINUTE +from .constants import SUNDAY +from .constants import W3C +from .constants import YEARS_PER_CENTURY +from .constants import YEARS_PER_DECADE +from .date import Date +from .exceptions import PendulumException +from .helpers import add_duration +from .helpers import timestamp +from .period import Period +from .time import Time +from .tz import UTC +from .tz.timezone import Timezone +from .utils._compat import _HAS_FOLD + + +_D = TypeVar("_D", bound="DateTime") + + +class DateTime(datetime.datetime, Date): + + EPOCH = None # type: DateTime + + # Formats + + _FORMATS = { + "atom": ATOM, + "cookie": COOKIE, + "iso8601": lambda dt: dt.isoformat(), + "rfc822": RFC822, + "rfc850": RFC850, + "rfc1036": RFC1036, + "rfc1123": RFC1123, + "rfc2822": RFC2822, + "rfc3339": lambda dt: dt.isoformat(), + "rss": RSS, + "w3c": W3C, + } + + _EPOCH = datetime.datetime(1970, 1, 1, tzinfo=UTC) + + _MODIFIERS_VALID_UNITS = [ + "second", + "minute", + "hour", + "day", + "week", + "month", + "year", + "decade", + "century", + ] + + if not _HAS_FOLD: + + def __new__( + cls, + year, + month, + day, + hour=0, + minute=0, + second=0, + microsecond=0, + tzinfo=None, + fold=0, + ): + self = datetime.datetime.__new__( + cls, year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo + ) + + self._fold = fold + + return self + + @classmethod + def now(cls, tz=None): # type: (Optional[Union[str, Timezone]]) -> DateTime + """ + Get a DateTime instance for the current date and time. + """ + return pendulum.now(tz) + + @classmethod + def utcnow(cls): # type: () -> DateTime + """ + Get a DateTime instance for the current date and time in UTC. + """ + return pendulum.now(UTC) + + @classmethod + def today(cls): # type: () -> DateTime + return pendulum.now() + + @classmethod + def strptime(cls, time, fmt): # type: (str, str) -> DateTime + return pendulum.instance(datetime.datetime.strptime(time, fmt)) + + # Getters/Setters + + def set( + self, + year=None, + month=None, + day=None, + hour=None, + minute=None, + second=None, + microsecond=None, + tz=None, + ): + if year is None: + year = self.year + if month is None: + month = self.month + if day is None: + day = self.day + if hour is None: + hour = self.hour + if minute is None: + minute = self.minute + if second is None: + second = self.second + if microsecond is None: + microsecond = self.microsecond + if tz is None: + tz = self.tz + + return pendulum.datetime( + year, month, day, hour, minute, second, microsecond, tz=tz + ) + + if not _HAS_FOLD: + + @property + def fold(self): + return self._fold + + def timestamp(self): + if self.tzinfo is None: + s = timestamp(self) + + return s + self.microsecond / 1e6 + else: + kwargs = {"tzinfo": self.tzinfo} + + if _HAS_FOLD: + kwargs["fold"] = self.fold + + dt = datetime.datetime( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + **kwargs + ) + return (dt - self._EPOCH).total_seconds() + + @property + def float_timestamp(self): + return self.timestamp() + + @property + def int_timestamp(self): + # Workaround needed to avoid inaccuracy + # for far into the future datetimes + kwargs = {"tzinfo": self.tzinfo} + + if _HAS_FOLD: + kwargs["fold"] = self.fold + + dt = datetime.datetime( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + **kwargs + ) + + delta = dt - self._EPOCH + + return delta.days * SECONDS_PER_DAY + delta.seconds + + @property + def offset(self): + return self.get_offset() + + @property + def offset_hours(self): + return self.get_offset() / SECONDS_PER_MINUTE / MINUTES_PER_HOUR + + @property + def timezone(self): # type: () -> Optional[Timezone] + if not isinstance(self.tzinfo, Timezone): + return + + return self.tzinfo + + @property + def tz(self): # type: () -> Optional[Timezone] + return self.timezone + + @property + def timezone_name(self): # type: () -> Optional[str] + tz = self.timezone + + if tz is None: + return None + + return tz.name + + @property + def age(self): + return self.date().diff(self.now(self.tz).date(), abs=False).in_years() + + def is_local(self): + return self.offset == self.in_timezone(pendulum.local_timezone()).offset + + def is_utc(self): + return self.offset == UTC.offset + + def is_dst(self): + return self.dst() != datetime.timedelta() + + def get_offset(self): + return int(self.utcoffset().total_seconds()) + + def date(self): + return Date(self.year, self.month, self.day) + + def time(self): + return Time(self.hour, self.minute, self.second, self.microsecond) + + def naive(self): # type: (_D) -> _D + """ + Return the DateTime without timezone information. + """ + return self.__class__( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + ) + + def on(self, year, month, day): + """ + Returns a new instance with the current date set to a different date. + + :param year: The year + :type year: int + + :param month: The month + :type month: int + + :param day: The day + :type day: int + + :rtype: DateTime + """ + return self.set(year=int(year), month=int(month), day=int(day)) + + def at(self, hour, minute=0, second=0, microsecond=0): + """ + Returns a new instance with the current time to a different time. + + :param hour: The hour + :type hour: int + + :param minute: The minute + :type minute: int + + :param second: The second + :type second: int + + :param microsecond: The microsecond + :type microsecond: int + + :rtype: DateTime + """ + return self.set( + hour=hour, minute=minute, second=second, microsecond=microsecond + ) + + def in_timezone(self, tz): # type: (Union[str, Timezone]) -> DateTime + """ + Set the instance's timezone from a string or object. + """ + tz = pendulum._safe_timezone(tz) + + return tz.convert(self, dst_rule=pendulum.POST_TRANSITION) + + def in_tz(self, tz): # type: (Union[str, Timezone]) -> DateTime + """ + Set the instance's timezone from a string or object. + """ + return self.in_timezone(tz) + + # STRING FORMATTING + + def to_time_string(self): + """ + Format the instance as time. + + :rtype: str + """ + return self.format("HH:mm:ss") + + def to_datetime_string(self): + """ + Format the instance as date and time. + + :rtype: str + """ + return self.format("YYYY-MM-DD HH:mm:ss") + + def to_day_datetime_string(self): + """ + Format the instance as day, date and time (in english). + + :rtype: str + """ + return self.format("ddd, MMM D, YYYY h:mm A", locale="en") + + def to_atom_string(self): + """ + Format the instance as ATOM. + + :rtype: str + """ + return self._to_string("atom") + + def to_cookie_string(self): + """ + Format the instance as COOKIE. + + :rtype: str + """ + return self._to_string("cookie", locale="en") + + def to_iso8601_string(self): + """ + Format the instance as ISO 8601. + + :rtype: str + """ + string = self._to_string("iso8601") + + if self.tz and self.tz.name == "UTC": + string = string.replace("+00:00", "Z") + + return string + + def to_rfc822_string(self): + """ + Format the instance as RFC 822. + + :rtype: str + """ + return self._to_string("rfc822") + + def to_rfc850_string(self): + """ + Format the instance as RFC 850. + + :rtype: str + """ + return self._to_string("rfc850") + + def to_rfc1036_string(self): + """ + Format the instance as RFC 1036. + + :rtype: str + """ + return self._to_string("rfc1036") + + def to_rfc1123_string(self): + """ + Format the instance as RFC 1123. + + :rtype: str + """ + return self._to_string("rfc1123") + + def to_rfc2822_string(self): + """ + Format the instance as RFC 2822. + + :rtype: str + """ + return self._to_string("rfc2822") + + def to_rfc3339_string(self): + """ + Format the instance as RFC 3339. + + :rtype: str + """ + return self._to_string("rfc3339") + + def to_rss_string(self): + """ + Format the instance as RSS. + + :rtype: str + """ + return self._to_string("rss") + + def to_w3c_string(self): + """ + Format the instance as W3C. + + :rtype: str + """ + return self._to_string("w3c") + + def _to_string(self, fmt, locale=None): + """ + Format the instance to a common string format. + + :param fmt: The name of the string format + :type fmt: string + + :param locale: The locale to use + :type locale: str or None + + :rtype: str + """ + if fmt not in self._FORMATS: + raise ValueError("Format [{}] is not supported".format(fmt)) + + fmt = self._FORMATS[fmt] + if callable(fmt): + return fmt(self) + + return self.format(fmt, locale=locale) + + def __str__(self): + return self.isoformat("T") + + def __repr__(self): + us = "" + if self.microsecond: + us = ", {}".format(self.microsecond) + + repr_ = "{klass}(" "{year}, {month}, {day}, " "{hour}, {minute}, {second}{us}" + + if self.tzinfo is not None: + repr_ += ", tzinfo={tzinfo}" + + repr_ += ")" + + return repr_.format( + klass=self.__class__.__name__, + year=self.year, + month=self.month, + day=self.day, + hour=self.hour, + minute=self.minute, + second=self.second, + us=us, + tzinfo=self.tzinfo, + ) + + # Comparisons + def closest(self, dt1, dt2, *dts): + """ + Get the farthest date from the instance. + + :type dt1: datetime.datetime + :type dt2: datetime.datetime + :type dts: list[datetime.datetime,] + + :rtype: DateTime + """ + dt1 = pendulum.instance(dt1) + dt2 = pendulum.instance(dt2) + dts = [dt1, dt2] + [pendulum.instance(x) for x in dts] + dts = [(abs(self - dt), dt) for dt in dts] + + return min(dts)[1] + + def farthest(self, dt1, dt2, *dts): + """ + Get the farthest date from the instance. + + :type dt1: datetime.datetime + :type dt2: datetime.datetime + :type dts: list[datetime.datetime,] + + :rtype: DateTime + """ + dt1 = pendulum.instance(dt1) + dt2 = pendulum.instance(dt2) + + dts = [dt1, dt2] + [pendulum.instance(x) for x in dts] + dts = [(abs(self - dt), dt) for dt in dts] + + return max(dts)[1] + + def is_future(self): + """ + Determines if the instance is in the future, ie. greater than now. + + :rtype: bool + """ + return self > self.now(self.timezone) + + def is_past(self): + """ + Determines if the instance is in the past, ie. less than now. + + :rtype: bool + """ + return self < self.now(self.timezone) + + def is_long_year(self): + """ + Determines if the instance is a long year + + See link `https://en.wikipedia.org/wiki/ISO_8601#Week_dates`_ + + :rtype: bool + """ + return ( + pendulum.datetime(self.year, 12, 28, 0, 0, 0, tz=self.tz).isocalendar()[1] + == 53 + ) + + def is_same_day(self, dt): + """ + Checks if the passed in date is the same day + as the instance current day. + + :type dt: DateTime or datetime or str or int + + :rtype: bool + """ + dt = pendulum.instance(dt) + + return self.to_date_string() == dt.to_date_string() + + def is_anniversary(self, dt=None): + """ + Check if its the anniversary. + Compares the date/month values of the two dates. + + :rtype: bool + """ + if dt is None: + dt = self.now(self.tz) + + instance = pendulum.instance(dt) + + return (self.month, self.day) == (instance.month, instance.day) + + # the additional method for checking if today is the anniversary day + # the alias is provided to start using a new name and keep the backward compatibility + # the old name can be completely replaced with the new in one of the future versions + is_birthday = is_anniversary + + # ADDITIONS AND SUBSTRACTIONS + + def add( + self, + years=0, + months=0, + weeks=0, + days=0, + hours=0, + minutes=0, + seconds=0, + microseconds=0, + ): # type: (_D, int, int, int, int, int, int, int, int) -> _D + """ + Add a duration to the instance. + + If we're adding units of variable length (i.e., years, months), + move forward from curren time, + otherwise move forward from utc, for accuracy + when moving across DST boundaries. + """ + units_of_variable_length = any([years, months, weeks, days]) + + current_dt = datetime.datetime( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + ) + if not units_of_variable_length: + offset = self.utcoffset() + if offset: + current_dt = current_dt - offset + + dt = add_duration( + current_dt, + years=years, + months=months, + weeks=weeks, + days=days, + hours=hours, + minutes=minutes, + seconds=seconds, + microseconds=microseconds, + ) + + if units_of_variable_length or self.tzinfo is None: + return pendulum.datetime( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tz=self.tz, + ) + + dt = self.__class__( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=UTC, + ) + + dt = self.tz.convert(dt) + + return self.__class__( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=self.tz, + fold=dt.fold, + ) + + def subtract( + self, + years=0, + months=0, + weeks=0, + days=0, + hours=0, + minutes=0, + seconds=0, + microseconds=0, + ): + """ + Remove duration from the instance. + + :param years: The number of years + :type years: int + + :param months: The number of months + :type months: int + + :param weeks: The number of weeks + :type weeks: int + + :param days: The number of days + :type days: int + + :param hours: The number of hours + :type hours: int + + :param minutes: The number of minutes + :type minutes: int + + :param seconds: The number of seconds + :type seconds: int + + :param microseconds: The number of microseconds + :type microseconds: int + + :rtype: DateTime + """ + return self.add( + years=-years, + months=-months, + weeks=-weeks, + days=-days, + hours=-hours, + minutes=-minutes, + seconds=-seconds, + microseconds=-microseconds, + ) + + # Adding a final underscore to the method name + # to avoid errors for PyPy which already defines + # a _add_timedelta method + def _add_timedelta_(self, delta): + """ + Add timedelta duration to the instance. + + :param delta: The timedelta instance + :type delta: pendulum.Duration or datetime.timedelta + + :rtype: DateTime + """ + if isinstance(delta, pendulum.Period): + return self.add( + years=delta.years, + months=delta.months, + weeks=delta.weeks, + days=delta.remaining_days, + hours=delta.hours, + minutes=delta.minutes, + seconds=delta.remaining_seconds, + microseconds=delta.microseconds, + ) + elif isinstance(delta, pendulum.Duration): + return self.add( + years=delta.years, months=delta.months, seconds=delta._total + ) + + return self.add(seconds=delta.total_seconds()) + + def _subtract_timedelta(self, delta): + """ + Remove timedelta duration from the instance. + + :param delta: The timedelta instance + :type delta: pendulum.Duration or datetime.timedelta + + :rtype: DateTime + """ + if isinstance(delta, pendulum.Duration): + return self.subtract( + years=delta.years, months=delta.months, seconds=delta._total + ) + + return self.subtract(seconds=delta.total_seconds()) + + # DIFFERENCES + + def diff(self, dt=None, abs=True): + """ + Returns the difference between two DateTime objects represented as a Duration. + + :type dt: DateTime or None + + :param abs: Whether to return an absolute interval or not + :type abs: bool + + :rtype: Period + """ + if dt is None: + dt = self.now(self.tz) + + return Period(self, dt, absolute=abs) + + def diff_for_humans( + self, + other=None, # type: Optional[DateTime] + absolute=False, # type: bool + locale=None, # type: Optional[str] + ): # type: (...) -> str + """ + Get the difference in a human readable format in the current locale. + + When comparing a value in the past to default now: + 1 day ago + 5 months ago + + When comparing a value in the future to default now: + 1 day from now + 5 months from now + + When comparing a value in the past to another value: + 1 day before + 5 months before + + When comparing a value in the future to another value: + 1 day after + 5 months after + """ + is_now = other is None + + if is_now: + other = self.now() + + diff = self.diff(other) + + return pendulum.format_diff(diff, is_now, absolute, locale) + + # Modifiers + def start_of(self, unit): + """ + Returns a copy of the instance with the time reset + with the following rules: + + * second: microsecond set to 0 + * minute: second and microsecond set to 0 + * hour: minute, second and microsecond set to 0 + * day: time to 00:00:00 + * week: date to first day of the week and time to 00:00:00 + * month: date to first day of the month and time to 00:00:00 + * year: date to first day of the year and time to 00:00:00 + * decade: date to first day of the decade and time to 00:00:00 + * century: date to first day of century and time to 00:00:00 + + :param unit: The unit to reset to + :type unit: str + + :rtype: DateTime + """ + if unit not in self._MODIFIERS_VALID_UNITS: + raise ValueError('Invalid unit "{}" for start_of()'.format(unit)) + + return getattr(self, "_start_of_{}".format(unit))() + + def end_of(self, unit): + """ + Returns a copy of the instance with the time reset + with the following rules: + + * second: microsecond set to 999999 + * minute: second set to 59 and microsecond set to 999999 + * hour: minute and second set to 59 and microsecond set to 999999 + * day: time to 23:59:59.999999 + * week: date to last day of the week and time to 23:59:59.999999 + * month: date to last day of the month and time to 23:59:59.999999 + * year: date to last day of the year and time to 23:59:59.999999 + * decade: date to last day of the decade and time to 23:59:59.999999 + * century: date to last day of century and time to 23:59:59.999999 + + :param unit: The unit to reset to + :type unit: str + + :rtype: DateTime + """ + if unit not in self._MODIFIERS_VALID_UNITS: + raise ValueError('Invalid unit "%s" for end_of()' % unit) + + return getattr(self, "_end_of_%s" % unit)() + + def _start_of_second(self): + """ + Reset microseconds to 0. + + :rtype: DateTime + """ + return self.set(microsecond=0) + + def _end_of_second(self): + """ + Set microseconds to 999999. + + :rtype: DateTime + """ + return self.set(microsecond=999999) + + def _start_of_minute(self): + """ + Reset seconds and microseconds to 0. + + :rtype: DateTime + """ + return self.set(second=0, microsecond=0) + + def _end_of_minute(self): + """ + Set seconds to 59 and microseconds to 999999. + + :rtype: DateTime + """ + return self.set(second=59, microsecond=999999) + + def _start_of_hour(self): + """ + Reset minutes, seconds and microseconds to 0. + + :rtype: DateTime + """ + return self.set(minute=0, second=0, microsecond=0) + + def _end_of_hour(self): + """ + Set minutes and seconds to 59 and microseconds to 999999. + + :rtype: DateTime + """ + return self.set(minute=59, second=59, microsecond=999999) + + def _start_of_day(self): + """ + Reset the time to 00:00:00 + + :rtype: DateTime + """ + return self.at(0, 0, 0, 0) + + def _end_of_day(self): + """ + Reset the time to 23:59:59.999999 + + :rtype: DateTime + """ + return self.at(23, 59, 59, 999999) + + def _start_of_month(self): + """ + Reset the date to the first day of the month and the time to 00:00:00. + + :rtype: DateTime + """ + return self.set(self.year, self.month, 1, 0, 0, 0, 0) + + def _end_of_month(self): + """ + Reset the date to the last day of the month + and the time to 23:59:59.999999. + + :rtype: DateTime + """ + return self.set(self.year, self.month, self.days_in_month, 23, 59, 59, 999999) + + def _start_of_year(self): + """ + Reset the date to the first day of the year and the time to 00:00:00. + + :rtype: DateTime + """ + return self.set(self.year, 1, 1, 0, 0, 0, 0) + + def _end_of_year(self): + """ + Reset the date to the last day of the year + and the time to 23:59:59.999999 + + :rtype: DateTime + """ + return self.set(self.year, 12, 31, 23, 59, 59, 999999) + + def _start_of_decade(self): + """ + Reset the date to the first day of the decade + and the time to 00:00:00. + + :rtype: DateTime + """ + year = self.year - self.year % YEARS_PER_DECADE + return self.set(year, 1, 1, 0, 0, 0, 0) + + def _end_of_decade(self): + """ + Reset the date to the last day of the decade + and the time to 23:59:59.999999. + + :rtype: DateTime + """ + year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1 + + return self.set(year, 12, 31, 23, 59, 59, 999999) + + def _start_of_century(self): + """ + Reset the date to the first day of the century + and the time to 00:00:00. + + :rtype: DateTime + """ + year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1 + + return self.set(year, 1, 1, 0, 0, 0, 0) + + def _end_of_century(self): + """ + Reset the date to the last day of the century + and the time to 23:59:59.999999. + + :rtype: DateTime + """ + year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY + + return self.set(year, 12, 31, 23, 59, 59, 999999) + + def _start_of_week(self): + """ + Reset the date to the first day of the week + and the time to 00:00:00. + + :rtype: DateTime + """ + dt = self + + if self.day_of_week != pendulum._WEEK_STARTS_AT: + dt = self.previous(pendulum._WEEK_STARTS_AT) + + return dt.start_of("day") + + def _end_of_week(self): + """ + Reset the date to the last day of the week + and the time to 23:59:59. + + :rtype: DateTime + """ + dt = self + + if self.day_of_week != pendulum._WEEK_ENDS_AT: + dt = self.next(pendulum._WEEK_ENDS_AT) + + return dt.end_of("day") + + def next(self, day_of_week=None, keep_time=False): + """ + Modify to the next occurrence of a given day of the week. + If no day_of_week is provided, modify to the next occurrence + of the current day of the week. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + + :param day_of_week: The next day of week to reset to. + :type day_of_week: int or None + + :param keep_time: Whether to keep the time information or not. + :type keep_time: bool + + :rtype: DateTime + """ + if day_of_week is None: + day_of_week = self.day_of_week + + if day_of_week < SUNDAY or day_of_week > SATURDAY: + raise ValueError("Invalid day of week") + + if keep_time: + dt = self + else: + dt = self.start_of("day") + + dt = dt.add(days=1) + while dt.day_of_week != day_of_week: + dt = dt.add(days=1) + + return dt + + def previous(self, day_of_week=None, keep_time=False): + """ + Modify to the previous occurrence of a given day of the week. + If no day_of_week is provided, modify to the previous occurrence + of the current day of the week. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + + :param day_of_week: The previous day of week to reset to. + :type day_of_week: int or None + + :param keep_time: Whether to keep the time information or not. + :type keep_time: bool + + :rtype: DateTime + """ + if day_of_week is None: + day_of_week = self.day_of_week + + if day_of_week < SUNDAY or day_of_week > SATURDAY: + raise ValueError("Invalid day of week") + + if keep_time: + dt = self + else: + dt = self.start_of("day") + + dt = dt.subtract(days=1) + while dt.day_of_week != day_of_week: + dt = dt.subtract(days=1) + + return dt + + def first_of(self, unit, day_of_week=None): + """ + Returns an instance set to the first occurrence + of a given day of the week in the current unit. + If no day_of_week is provided, modify to the first day of the unit. + Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY. + + Supported units are month, quarter and year. + + :param unit: The unit to use + :type unit: str + + :type day_of_week: int or None + + :rtype: DateTime + """ + if unit not in ["month", "quarter", "year"]: + raise ValueError('Invalid unit "{}" for first_of()'.format(unit)) + + return getattr(self, "_first_of_{}".format(unit))(day_of_week) + + def last_of(self, unit, day_of_week=None): + """ + Returns an instance set to the last occurrence + of a given day of the week in the current unit. + If no day_of_week is provided, modify to the last day of the unit. + Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY. + + Supported units are month, quarter and year. + + :param unit: The unit to use + :type unit: str + + :type day_of_week: int or None + + :rtype: DateTime + """ + if unit not in ["month", "quarter", "year"]: + raise ValueError('Invalid unit "{}" for first_of()'.format(unit)) + + return getattr(self, "_last_of_{}".format(unit))(day_of_week) + + def nth_of(self, unit, nth, day_of_week): + """ + Returns a new instance set to the given occurrence + of a given day of the week in the current unit. + If the calculated occurrence is outside the scope of the current unit, + then raise an error. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + + Supported units are month, quarter and year. + + :param unit: The unit to use + :type unit: str + + :type nth: int + + :type day_of_week: int or None + + :rtype: DateTime + """ + if unit not in ["month", "quarter", "year"]: + raise ValueError('Invalid unit "{}" for first_of()'.format(unit)) + + dt = getattr(self, "_nth_of_{}".format(unit))(nth, day_of_week) + if dt is False: + raise PendulumException( + "Unable to find occurence {} of {} in {}".format( + nth, self._days[day_of_week], unit + ) + ) + + return dt + + def _first_of_month(self, day_of_week): + """ + Modify to the first occurrence of a given day of the week + in the current month. If no day_of_week is provided, + modify to the first day of the month. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + + :type day_of_week: int + + :rtype: DateTime + """ + dt = self.start_of("day") + + if day_of_week is None: + return dt.set(day=1) + + month = calendar.monthcalendar(dt.year, dt.month) + + calendar_day = (day_of_week - 1) % 7 + + if month[0][calendar_day] > 0: + day_of_month = month[0][calendar_day] + else: + day_of_month = month[1][calendar_day] + + return dt.set(day=day_of_month) + + def _last_of_month(self, day_of_week=None): + """ + Modify to the last occurrence of a given day of the week + in the current month. If no day_of_week is provided, + modify to the last day of the month. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + + :type day_of_week: int or None + + :rtype: DateTime + """ + dt = self.start_of("day") + + if day_of_week is None: + return dt.set(day=self.days_in_month) + + month = calendar.monthcalendar(dt.year, dt.month) + + calendar_day = (day_of_week - 1) % 7 + + if month[-1][calendar_day] > 0: + day_of_month = month[-1][calendar_day] + else: + day_of_month = month[-2][calendar_day] + + return dt.set(day=day_of_month) + + def _nth_of_month(self, nth, day_of_week): + """ + Modify to the given occurrence of a given day of the week + in the current month. If the calculated occurrence is outside, + the scope of the current month, then return False and no + modifications are made. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + + :type nth: int + + :type day_of_week: int or None + + :rtype: DateTime + """ + if nth == 1: + return self.first_of("month", day_of_week) + + dt = self.first_of("month") + check = dt.format("%Y-%M") + for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + dt = dt.next(day_of_week) + + if dt.format("%Y-%M") == check: + return self.set(day=dt.day).start_of("day") + + return False + + def _first_of_quarter(self, day_of_week=None): + """ + Modify to the first occurrence of a given day of the week + in the current quarter. If no day_of_week is provided, + modify to the first day of the quarter. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + + :type day_of_week: int or None + + :rtype: DateTime + """ + return self.on(self.year, self.quarter * 3 - 2, 1).first_of( + "month", day_of_week + ) + + def _last_of_quarter(self, day_of_week=None): + """ + Modify to the last occurrence of a given day of the week + in the current quarter. If no day_of_week is provided, + modify to the last day of the quarter. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + + :type day_of_week: int or None + + :rtype: DateTime + """ + return self.on(self.year, self.quarter * 3, 1).last_of("month", day_of_week) + + def _nth_of_quarter(self, nth, day_of_week): + """ + Modify to the given occurrence of a given day of the week + in the current quarter. If the calculated occurrence is outside, + the scope of the current quarter, then return False and no + modifications are made. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + + :type nth: int + + :type day_of_week: int or None + + :rtype: DateTime + """ + if nth == 1: + return self.first_of("quarter", day_of_week) + + dt = self.set(day=1, month=self.quarter * 3) + last_month = dt.month + year = dt.year + dt = dt.first_of("quarter") + for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + dt = dt.next(day_of_week) + + if last_month < dt.month or year != dt.year: + return False + + return self.on(self.year, dt.month, dt.day).start_of("day") + + def _first_of_year(self, day_of_week=None): + """ + Modify to the first occurrence of a given day of the week + in the current year. If no day_of_week is provided, + modify to the first day of the year. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + + :type day_of_week: int or None + + :rtype: DateTime + """ + return self.set(month=1).first_of("month", day_of_week) + + def _last_of_year(self, day_of_week=None): + """ + Modify to the last occurrence of a given day of the week + in the current year. If no day_of_week is provided, + modify to the last day of the year. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + + :type day_of_week: int or None + + :rtype: DateTime + """ + return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week) + + def _nth_of_year(self, nth, day_of_week): + """ + Modify to the given occurrence of a given day of the week + in the current year. If the calculated occurrence is outside, + the scope of the current year, then return False and no + modifications are made. Use the supplied consts + to indicate the desired day_of_week, ex. DateTime.MONDAY. + + :type nth: int + + :type day_of_week: int or None + + :rtype: DateTime + """ + if nth == 1: + return self.first_of("year", day_of_week) + + dt = self.first_of("year") + year = dt.year + for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + dt = dt.next(day_of_week) + + if year != dt.year: + return False + + return self.on(self.year, dt.month, dt.day).start_of("day") + + def average(self, dt=None): + """ + Modify the current instance to the average + of a given instance (default now) and the current instance. + + :type dt: DateTime or datetime + + :rtype: DateTime + """ + if dt is None: + dt = self.now(self.tz) + + diff = self.diff(dt, False) + return self.add( + microseconds=(diff.in_seconds() * 1000000 + diff.microseconds) // 2 + ) + + def __sub__(self, other): + if isinstance(other, datetime.timedelta): + return self._subtract_timedelta(other) + + if not isinstance(other, datetime.datetime): + return NotImplemented + + if not isinstance(other, self.__class__): + if other.tzinfo is None: + other = pendulum.naive( + other.year, + other.month, + other.day, + other.hour, + other.minute, + other.second, + other.microsecond, + ) + else: + other = pendulum.instance(other) + + return other.diff(self, False) + + def __rsub__(self, other): + if not isinstance(other, datetime.datetime): + return NotImplemented + + if not isinstance(other, self.__class__): + if other.tzinfo is None: + other = pendulum.naive( + other.year, + other.month, + other.day, + other.hour, + other.minute, + other.second, + other.microsecond, + ) + else: + other = pendulum.instance(other) + + return self.diff(other, False) + + def __add__(self, other): + if not isinstance(other, datetime.timedelta): + return NotImplemented + + return self._add_timedelta_(other) + + def __radd__(self, other): + return self.__add__(other) + + # Native methods override + + @classmethod + def fromtimestamp(cls, t, tz=None): + return pendulum.instance(datetime.datetime.fromtimestamp(t, tz=tz), tz=tz) + + @classmethod + def utcfromtimestamp(cls, t): + return pendulum.instance(datetime.datetime.utcfromtimestamp(t), tz=None) + + @classmethod + def fromordinal(cls, n): + return pendulum.instance(datetime.datetime.fromordinal(n), tz=None) + + @classmethod + def combine(cls, date, time): + return pendulum.instance(datetime.datetime.combine(date, time), tz=None) + + def astimezone(self, tz=None): + return pendulum.instance(super(DateTime, self).astimezone(tz)) + + def replace( + self, + year=None, + month=None, + day=None, + hour=None, + minute=None, + second=None, + microsecond=None, + tzinfo=True, + fold=None, + ): + if year is None: + year = self.year + if month is None: + month = self.month + if day is None: + day = self.day + if hour is None: + hour = self.hour + if minute is None: + minute = self.minute + if second is None: + second = self.second + if microsecond is None: + microsecond = self.microsecond + if tzinfo is True: + tzinfo = self.tzinfo + if fold is None: + fold = self.fold + + transition_rule = pendulum.POST_TRANSITION + if fold is not None: + transition_rule = pendulum.PRE_TRANSITION + if fold: + transition_rule = pendulum.POST_TRANSITION + + return pendulum.datetime( + year, + month, + day, + hour, + minute, + second, + microsecond, + tz=tzinfo, + dst_rule=transition_rule, + ) + + def __getnewargs__(self): + return (self,) + + def _getstate(self, protocol=3): + return ( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + self.tzinfo, + ) + + def __reduce__(self): + return self.__reduce_ex__(2) + + def __reduce_ex__(self, protocol): + return self.__class__, self._getstate(protocol) + + def _cmp(self, other, **kwargs): + # Fix for pypy which compares using this method + # which would lead to infinite recursion if we didn't override + kwargs = {"tzinfo": self.tz} + + if _HAS_FOLD: + kwargs["fold"] = self.fold + + dt = datetime.datetime( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + **kwargs + ) + + return 0 if dt == other else 1 if dt > other else -1 + + +DateTime.min = DateTime(1, 1, 1, 0, 0, tzinfo=UTC) +DateTime.max = DateTime(9999, 12, 31, 23, 59, 59, 999999, tzinfo=UTC) +DateTime.EPOCH = DateTime(1970, 1, 1) diff --git a/pendulum/duration.py b/pendulum/duration.py new file mode 100644 index 0000000..45df13d --- /dev/null +++ b/pendulum/duration.py @@ -0,0 +1,479 @@ +from __future__ import absolute_import +from __future__ import division + +from datetime import timedelta + +import pendulum + +from pendulum.utils._compat import PYPY +from pendulum.utils._compat import decode + +from .constants import SECONDS_PER_DAY +from .constants import SECONDS_PER_HOUR +from .constants import SECONDS_PER_MINUTE +from .constants import US_PER_SECOND + + +def _divide_and_round(a, b): + """divide a by b and round result to the nearest integer + + When the ratio is exactly half-way between two integers, + the even integer is returned. + """ + # Based on the reference implementation for divmod_near + # in Objects/longobject.c. + q, r = divmod(a, b) + # round up if either r / b > 0.5, or r / b == 0.5 and q is odd. + # The expression r / b > 0.5 is equivalent to 2 * r > b if b is + # positive, 2 * r < b if b negative. + r *= 2 + greater_than_half = r > b if b > 0 else r < b + if greater_than_half or r == b and q % 2 == 1: + q += 1 + + return q + + +class Duration(timedelta): + """ + Replacement for the standard timedelta class. + + Provides several improvements over the base class. + """ + + _y = None + _m = None + _w = None + _d = None + _h = None + _i = None + _s = None + _invert = None + + def __new__( + cls, + days=0, + seconds=0, + microseconds=0, + milliseconds=0, + minutes=0, + hours=0, + weeks=0, + years=0, + months=0, + ): + if not isinstance(years, int) or not isinstance(months, int): + raise ValueError("Float year and months are not supported") + + self = timedelta.__new__( + cls, + days + years * 365 + months * 30, + seconds, + microseconds, + milliseconds, + minutes, + hours, + weeks, + ) + + # Intuitive normalization + total = self.total_seconds() - (years * 365 + months * 30) * SECONDS_PER_DAY + self._total = total + + m = 1 + if total < 0: + m = -1 + + self._microseconds = round(total % m * 1e6) + self._seconds = abs(int(total)) % SECONDS_PER_DAY * m + + _days = abs(int(total)) // SECONDS_PER_DAY * m + self._days = _days + self._remaining_days = abs(_days) % 7 * m + self._weeks = abs(_days) // 7 * m + self._months = months + self._years = years + + return self + + def total_minutes(self): + return self.total_seconds() / SECONDS_PER_MINUTE + + def total_hours(self): + return self.total_seconds() / SECONDS_PER_HOUR + + def total_days(self): + return self.total_seconds() / SECONDS_PER_DAY + + def total_weeks(self): + return self.total_days() / 7 + + if PYPY: + + def total_seconds(self): + days = 0 + + if hasattr(self, "_years"): + days += self._years * 365 + + if hasattr(self, "_months"): + days += self._months * 30 + + if hasattr(self, "_remaining_days"): + days += self._weeks * 7 + self._remaining_days + else: + days += self._days + + return ( + (days * SECONDS_PER_DAY + self._seconds) * US_PER_SECOND + + self._microseconds + ) / US_PER_SECOND + + @property + def years(self): + return self._years + + @property + def months(self): + return self._months + + @property + def weeks(self): + return self._weeks + + if PYPY: + + @property + def days(self): + return self._years * 365 + self._months * 30 + self._days + + @property + def remaining_days(self): + return self._remaining_days + + @property + def hours(self): + if self._h is None: + seconds = self._seconds + self._h = 0 + if abs(seconds) >= 3600: + self._h = (abs(seconds) // 3600 % 24) * self._sign(seconds) + + return self._h + + @property + def minutes(self): + if self._i is None: + seconds = self._seconds + self._i = 0 + if abs(seconds) >= 60: + self._i = (abs(seconds) // 60 % 60) * self._sign(seconds) + + return self._i + + @property + def seconds(self): + return self._seconds + + @property + def remaining_seconds(self): + if self._s is None: + self._s = self._seconds + self._s = abs(self._s) % 60 * self._sign(self._s) + + return self._s + + @property + def microseconds(self): + return self._microseconds + + @property + def invert(self): + if self._invert is None: + self._invert = self.total_seconds() < 0 + + return self._invert + + def in_weeks(self): + return int(self.total_weeks()) + + def in_days(self): + return int(self.total_days()) + + def in_hours(self): + return int(self.total_hours()) + + def in_minutes(self): + return int(self.total_minutes()) + + def in_seconds(self): + return int(self.total_seconds()) + + def in_words(self, locale=None, separator=" "): + """ + Get the current interval in words in the current locale. + + Ex: 6 jours 23 heures 58 minutes + + :param locale: The locale to use. Defaults to current locale. + :type locale: str + + :param separator: The separator to use between each unit + :type separator: str + + :rtype: str + """ + periods = [ + ("year", self.years), + ("month", self.months), + ("week", self.weeks), + ("day", self.remaining_days), + ("hour", self.hours), + ("minute", self.minutes), + ("second", self.remaining_seconds), + ] + + if locale is None: + locale = pendulum.get_locale() + + locale = pendulum.locale(locale) + parts = [] + for period in periods: + unit, count = period + if abs(count) > 0: + translation = locale.translation( + "units.{}.{}".format(unit, locale.plural(abs(count))) + ) + parts.append(translation.format(count)) + + if not parts: + if abs(self.microseconds) > 0: + unit = "units.second.{}".format(locale.plural(1)) + count = "{:.2f}".format(abs(self.microseconds) / 1e6) + else: + unit = "units.microsecond.{}".format(locale.plural(0)) + count = 0 + translation = locale.translation(unit) + parts.append(translation.format(count)) + + return decode(separator.join(parts)) + + def _sign(self, value): + if value < 0: + return -1 + + return 1 + + def as_timedelta(self): + """ + Return the interval as a native timedelta. + + :rtype: timedelta + """ + return timedelta(seconds=self.total_seconds()) + + def __str__(self): + return self.in_words() + + def __repr__(self): + rep = "{}(".format(self.__class__.__name__) + + if self._years: + rep += "years={}, ".format(self._years) + + if self._months: + rep += "months={}, ".format(self._months) + + if self._weeks: + rep += "weeks={}, ".format(self._weeks) + + if self._days: + rep += "days={}, ".format(self._remaining_days) + + if self.hours: + rep += "hours={}, ".format(self.hours) + + if self.minutes: + rep += "minutes={}, ".format(self.minutes) + + if self.remaining_seconds: + rep += "seconds={}, ".format(self.remaining_seconds) + + if self.microseconds: + rep += "microseconds={}, ".format(self.microseconds) + + rep += ")" + + return rep.replace(", )", ")") + + def __add__(self, other): + if isinstance(other, timedelta): + return self.__class__(seconds=self.total_seconds() + other.total_seconds()) + + return NotImplemented + + __radd__ = __add__ + + def __sub__(self, other): + if isinstance(other, timedelta): + return self.__class__(seconds=self.total_seconds() - other.total_seconds()) + + return NotImplemented + + def __neg__(self): + return self.__class__( + years=-self._years, + months=-self._months, + weeks=-self._weeks, + days=-self._remaining_days, + seconds=-self._seconds, + microseconds=-self._microseconds, + ) + + def _to_microseconds(self): + return (self._days * (24 * 3600) + self._seconds) * 1000000 + self._microseconds + + def __mul__(self, other): + if isinstance(other, int): + return self.__class__( + years=self._years * other, + months=self._months * other, + seconds=self._total * other, + ) + + if isinstance(other, float): + usec = self._to_microseconds() + a, b = other.as_integer_ratio() + + return self.__class__(0, 0, _divide_and_round(usec * a, b)) + + return NotImplemented + + __rmul__ = __mul__ + + def __floordiv__(self, other): + if not isinstance(other, (int, timedelta)): + return NotImplemented + + usec = self._to_microseconds() + if isinstance(other, timedelta): + return usec // other._to_microseconds() + + if isinstance(other, int): + return self.__class__( + 0, + 0, + usec // other, + years=self._years // other, + months=self._months // other, + ) + + def __truediv__(self, other): + if not isinstance(other, (int, float, timedelta)): + return NotImplemented + + usec = self._to_microseconds() + if isinstance(other, timedelta): + return usec / other._to_microseconds() + + if isinstance(other, int): + return self.__class__( + 0, + 0, + _divide_and_round(usec, other), + years=_divide_and_round(self._years, other), + months=_divide_and_round(self._months, other), + ) + + if isinstance(other, float): + a, b = other.as_integer_ratio() + + return self.__class__( + 0, + 0, + _divide_and_round(b * usec, a), + years=_divide_and_round(self._years * b, a), + months=_divide_and_round(self._months, other), + ) + + __div__ = __floordiv__ + + def __mod__(self, other): + if isinstance(other, timedelta): + r = self._to_microseconds() % other._to_microseconds() + + return self.__class__(0, 0, r) + + return NotImplemented + + def __divmod__(self, other): + if isinstance(other, timedelta): + q, r = divmod(self._to_microseconds(), other._to_microseconds()) + + return q, self.__class__(0, 0, r) + + return NotImplemented + + +Duration.min = Duration(days=-999999999) +Duration.max = Duration( + days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999 +) +Duration.resolution = Duration(microseconds=1) + + +class AbsoluteDuration(Duration): + """ + Duration that expresses a time difference in absolute values. + """ + + def __new__( + cls, + days=0, + seconds=0, + microseconds=0, + milliseconds=0, + minutes=0, + hours=0, + weeks=0, + years=0, + months=0, + ): + if not isinstance(years, int) or not isinstance(months, int): + raise ValueError("Float year and months are not supported") + + self = timedelta.__new__( + cls, days, seconds, microseconds, milliseconds, minutes, hours, weeks + ) + + # We need to compute the total_seconds() value + # on a native timedelta object + delta = timedelta( + days, seconds, microseconds, milliseconds, minutes, hours, weeks + ) + + # Intuitive normalization + self._total = delta.total_seconds() + total = abs(self._total) + + self._microseconds = round(total % 1 * 1e6) + self._seconds = int(total) % SECONDS_PER_DAY + + days = int(total) // SECONDS_PER_DAY + self._days = abs(days + years * 365 + months * 30) + self._remaining_days = days % 7 + self._weeks = days // 7 + self._months = abs(months) + self._years = abs(years) + + return self + + def total_seconds(self): + return abs(self._total) + + @property + def invert(self): + if self._invert is None: + self._invert = self._total < 0 + + return self._invert diff --git a/pendulum/exceptions.py b/pendulum/exceptions.py new file mode 100644 index 0000000..4c6448a --- /dev/null +++ b/pendulum/exceptions.py @@ -0,0 +1,6 @@ +from .parsing.exceptions import ParserError # noqa + + +class PendulumException(Exception): + + pass diff --git a/pendulum/formatting/__init__.py b/pendulum/formatting/__init__.py new file mode 100644 index 0000000..856321a --- /dev/null +++ b/pendulum/formatting/__init__.py @@ -0,0 +1,4 @@ +from .formatter import Formatter + + +__all__ = ["Formatter"] diff --git a/pendulum/formatting/difference_formatter.py b/pendulum/formatting/difference_formatter.py new file mode 100644 index 0000000..9be4b96 --- /dev/null +++ b/pendulum/formatting/difference_formatter.py @@ -0,0 +1,153 @@ +import typing + +import pendulum + +from pendulum.utils._compat import decode + +from ..locales.locale import Locale + + +class DifferenceFormatter(object): + """ + Handles formatting differences in text. + """ + + def __init__(self, locale="en"): + self._locale = Locale.load(locale) + + def format( + self, diff, is_now=True, absolute=False, locale=None + ): # type: (pendulum.Period, bool, bool, typing.Optional[str]) -> str + """ + Formats a difference. + + :param diff: The difference to format + :type diff: pendulum.period.Period + + :param is_now: Whether the difference includes now + :type is_now: bool + + :param absolute: Whether it's an absolute difference or not + :type absolute: bool + + :param locale: The locale to use + :type locale: str or None + + :rtype: str + """ + if locale is None: + locale = self._locale + else: + locale = Locale.load(locale) + + count = diff.remaining_seconds + + if diff.years > 0: + unit = "year" + count = diff.years + + if diff.months > 6: + count += 1 + elif diff.months == 11 and (diff.weeks * 7 + diff.remaining_days) > 15: + unit = "year" + count = 1 + elif diff.months > 0: + unit = "month" + count = diff.months + + if (diff.weeks * 7 + diff.remaining_days) >= 27: + count += 1 + elif diff.weeks > 0: + unit = "week" + count = diff.weeks + + if diff.remaining_days > 3: + count += 1 + elif diff.remaining_days > 0: + unit = "day" + count = diff.remaining_days + + if diff.hours >= 22: + count += 1 + elif diff.hours > 0: + unit = "hour" + count = diff.hours + elif diff.minutes > 0: + unit = "minute" + count = diff.minutes + elif 10 < diff.remaining_seconds <= 59: + unit = "second" + count = diff.remaining_seconds + else: + # We check if the "a few seconds" unit exists + time = locale.get("custom.units.few_second") + if time is not None: + if absolute: + return time + + key = "custom" + is_future = diff.invert + if is_now: + if is_future: + key += ".from_now" + else: + key += ".ago" + else: + if is_future: + key += ".after" + else: + key += ".before" + + return locale.get(key).format(time) + else: + unit = "second" + count = diff.remaining_seconds + + if count == 0: + count = 1 + + if absolute: + key = "translations.units.{}".format(unit) + else: + is_future = diff.invert + + if is_now: + # Relative to now, so we can use + # the CLDR data + key = "translations.relative.{}".format(unit) + + if is_future: + key += ".future" + else: + key += ".past" + else: + # Absolute comparison + # So we have to use the custom locale data + + # Checking for special pluralization rules + key = "custom.units_relative" + if is_future: + key += ".{}.future".format(unit) + else: + key += ".{}.past".format(unit) + + trans = locale.get(key) + if not trans: + # No special rule + time = locale.get( + "translations.units.{}.{}".format(unit, locale.plural(count)) + ).format(count) + else: + time = trans[locale.plural(count)].format(count) + + key = "custom" + if is_future: + key += ".after" + else: + key += ".before" + + return locale.get(key).format(decode(time)) + + key += ".{}".format(locale.plural(count)) + + return decode(locale.get(key).format(count)) diff --git a/pendulum/formatting/formatter.py b/pendulum/formatting/formatter.py new file mode 100644 index 0000000..9b64fcd --- /dev/null +++ b/pendulum/formatting/formatter.py @@ -0,0 +1,685 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import datetime +import re +import typing + +import pendulum + +from pendulum.locales.locale import Locale +from pendulum.utils._compat import decode + + +_MATCH_1 = r"\d" +_MATCH_2 = r"\d\d" +_MATCH_3 = r"\d{3}" +_MATCH_4 = r"\d{4}" +_MATCH_6 = r"[+-]?\d{6}" +_MATCH_1_TO_2 = r"\d\d?" +_MATCH_1_TO_2_LEFT_PAD = r"[0-9 ]\d?" +_MATCH_1_TO_3 = r"\d{1,3}" +_MATCH_1_TO_4 = r"\d{1,4}" +_MATCH_1_TO_6 = r"[+-]?\d{1,6}" +_MATCH_3_TO_4 = r"\d{3}\d?" +_MATCH_5_TO_6 = r"\d{5}\d?" +_MATCH_UNSIGNED = r"\d+" +_MATCH_SIGNED = r"[+-]?\d+" +_MATCH_OFFSET = r"[Zz]|[+-]\d\d:?\d\d" +_MATCH_SHORT_OFFSET = r"[Zz]|[+-]\d\d(?::?\d\d)?" +_MATCH_TIMESTAMP = r"[+-]?\d+(\.\d{1,6})?" +_MATCH_WORD = ( + "(?i)[0-9]*" + "['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+" + r"|[\u0600-\u06FF/]+(\s*?[\u0600-\u06FF]+){1,2}" +) +_MATCH_TIMEZONE = "[A-Za-z0-9-+]+(/[A-Za-z0-9-+_]+)?" + + +class Formatter: + + _TOKENS = ( + r"\[([^\[]*)\]|\\(.)|" + "(" + "Mo|MM?M?M?" + "|Do|DDDo|DD?D?D?|ddd?d?|do?" + "|E{1,4}" + "|w[o|w]?|W[o|W]?|Qo?" + "|YYYY|YY|Y" + "|gg(ggg?)?|GG(GGG?)?" + "|a|A" + "|hh?|HH?|kk?" + "|mm?|ss?|S{1,9}" + "|x|X" + "|zz?|ZZ?" + "|LTS|LT|LL?L?L?" + ")" + ) + + _FORMAT_RE = re.compile(_TOKENS) + + _FROM_FORMAT_RE = re.compile(r"(?<!\\\[)" + _TOKENS + r"(?!\\\])") + + _LOCALIZABLE_TOKENS = { + "Qo": None, + "MMMM": "months.wide", + "MMM": "months.abbreviated", + "Mo": None, + "DDDo": None, + "Do": lambda locale: tuple( + r"\d+{}".format(o) for o in locale.get("custom.ordinal").values() + ), + "dddd": "days.wide", + "ddd": "days.abbreviated", + "dd": "days.short", + "do": None, + "Wo": None, + "wo": None, + "A": lambda locale: ( + locale.translation("day_periods.am"), + locale.translation("day_periods.pm"), + ), + "a": lambda locale: ( + locale.translation("day_periods.am").lower(), + locale.translation("day_periods.pm").lower(), + ), + } + + _TOKENS_RULES = { + # Year + "YYYY": lambda dt: "{:d}".format(dt.year), + "YY": lambda dt: "{:d}".format(dt.year)[2:], + "Y": lambda dt: "{:d}".format(dt.year), + # Quarter + "Q": lambda dt: "{:d}".format(dt.quarter), + # Month + "MM": lambda dt: "{:02d}".format(dt.month), + "M": lambda dt: "{:d}".format(dt.month), + # Day + "DD": lambda dt: "{:02d}".format(dt.day), + "D": lambda dt: "{:d}".format(dt.day), + # Day of Year + "DDDD": lambda dt: "{:03d}".format(dt.day_of_year), + "DDD": lambda dt: "{:d}".format(dt.day_of_year), + # Day of Week + "d": lambda dt: "{:d}".format(dt.day_of_week), + # Day of ISO Week + "E": lambda dt: "{:d}".format(dt.isoweekday()), + # Hour + "HH": lambda dt: "{:02d}".format(dt.hour), + "H": lambda dt: "{:d}".format(dt.hour), + "hh": lambda dt: "{:02d}".format(dt.hour % 12 or 12), + "h": lambda dt: "{:d}".format(dt.hour % 12 or 12), + # Minute + "mm": lambda dt: "{:02d}".format(dt.minute), + "m": lambda dt: "{:d}".format(dt.minute), + # Second + "ss": lambda dt: "{:02d}".format(dt.second), + "s": lambda dt: "{:d}".format(dt.second), + # Fractional second + "S": lambda dt: "{:01d}".format(dt.microsecond // 100000), + "SS": lambda dt: "{:02d}".format(dt.microsecond // 10000), + "SSS": lambda dt: "{:03d}".format(dt.microsecond // 1000), + "SSSS": lambda dt: "{:04d}".format(dt.microsecond // 100), + "SSSSS": lambda dt: "{:05d}".format(dt.microsecond // 10), + "SSSSSS": lambda dt: "{:06d}".format(dt.microsecond), + # Timestamp + "X": lambda dt: "{:d}".format(dt.int_timestamp), + "x": lambda dt: "{:d}".format(dt.int_timestamp * 1000 + dt.microsecond // 1000), + # Timezone + "zz": lambda dt: "{}".format(dt.tzname() if dt.tzinfo is not None else ""), + "z": lambda dt: "{}".format(dt.timezone_name or ""), + } + + _DATE_FORMATS = { + "LTS": "formats.time.full", + "LT": "formats.time.short", + "L": "formats.date.short", + "LL": "formats.date.long", + "LLL": "formats.datetime.long", + "LLLL": "formats.datetime.full", + } + + _DEFAULT_DATE_FORMATS = { + "LTS": "h:mm:ss A", + "LT": "h:mm A", + "L": "MM/DD/YYYY", + "LL": "MMMM D, YYYY", + "LLL": "MMMM D, YYYY h:mm A", + "LLLL": "dddd, MMMM D, YYYY h:mm A", + } + + _REGEX_TOKENS = { + "Y": _MATCH_SIGNED, + "YY": (_MATCH_1_TO_2, _MATCH_2), + "YYYY": (_MATCH_1_TO_4, _MATCH_4), + "Q": _MATCH_1, + "Qo": None, + "M": _MATCH_1_TO_2, + "MM": (_MATCH_1_TO_2, _MATCH_2), + "MMM": _MATCH_WORD, + "MMMM": _MATCH_WORD, + "D": _MATCH_1_TO_2, + "DD": (_MATCH_1_TO_2_LEFT_PAD, _MATCH_2), + "DDD": _MATCH_1_TO_3, + "DDDD": _MATCH_3, + "dddd": _MATCH_WORD, + "ddd": _MATCH_WORD, + "dd": _MATCH_WORD, + "d": _MATCH_1, + "E": _MATCH_1, + "Do": None, + "H": _MATCH_1_TO_2, + "HH": (_MATCH_1_TO_2, _MATCH_2), + "h": _MATCH_1_TO_2, + "hh": (_MATCH_1_TO_2, _MATCH_2), + "m": _MATCH_1_TO_2, + "mm": (_MATCH_1_TO_2, _MATCH_2), + "s": _MATCH_1_TO_2, + "ss": (_MATCH_1_TO_2, _MATCH_2), + "S": (_MATCH_1_TO_3, _MATCH_1), + "SS": (_MATCH_1_TO_3, _MATCH_2), + "SSS": (_MATCH_1_TO_3, _MATCH_3), + "SSSS": _MATCH_UNSIGNED, + "SSSSS": _MATCH_UNSIGNED, + "SSSSSS": _MATCH_UNSIGNED, + "x": _MATCH_SIGNED, + "X": _MATCH_TIMESTAMP, + "ZZ": _MATCH_SHORT_OFFSET, + "Z": _MATCH_OFFSET, + "z": _MATCH_TIMEZONE, + } + + _PARSE_TOKENS = { + "YYYY": lambda year: int(year), + "YY": lambda year: int(year), + "Q": lambda quarter: int(quarter), + "MMMM": lambda month: month, + "MMM": lambda month: month, + "MM": lambda month: int(month), + "M": lambda month: int(month), + "DDDD": lambda day: int(day), + "DDD": lambda day: int(day), + "DD": lambda day: int(day), + "D": lambda day: int(day), + "dddd": lambda weekday: weekday, + "ddd": lambda weekday: weekday, + "dd": lambda weekday: weekday, + "d": lambda weekday: int(weekday) % 7, + "E": lambda weekday: int(weekday), + "HH": lambda hour: int(hour), + "H": lambda hour: int(hour), + "hh": lambda hour: int(hour), + "h": lambda hour: int(hour), + "mm": lambda minute: int(minute), + "m": lambda minute: int(minute), + "ss": lambda second: int(second), + "s": lambda second: int(second), + "S": lambda us: int(us) * 100000, + "SS": lambda us: int(us) * 10000, + "SSS": lambda us: int(us) * 1000, + "SSSS": lambda us: int(us) * 100, + "SSSSS": lambda us: int(us) * 10, + "SSSSSS": lambda us: int(us), + "a": lambda meridiem: meridiem, + "X": lambda ts: float(ts), + "x": lambda ts: float(ts) / 1e3, + "ZZ": str, + "Z": str, + "z": str, + } + + def format( + self, dt, fmt, locale=None + ): # type: (pendulum.DateTime, str, typing.Optional[typing.Union[str, Locale]]) -> str + """ + Formats a DateTime instance with a given format and locale. + + :param dt: The instance to format + :type dt: pendulum.DateTime + + :param fmt: The format to use + :type fmt: str + + :param locale: The locale to use + :type locale: str or Locale or None + + :rtype: str + """ + if not locale: + locale = pendulum.get_locale() + + locale = Locale.load(locale) + + result = self._FORMAT_RE.sub( + lambda m: m.group(1) + if m.group(1) + else m.group(2) + if m.group(2) + else self._format_token(dt, m.group(3), locale), + fmt, + ) + + return decode(result) + + def _format_token( + self, dt, token, locale + ): # type: (pendulum.DateTime, str, Locale) -> str + """ + Formats a DateTime instance with a given token and locale. + + :param dt: The instance to format + :type dt: pendulum.DateTime + + :param token: The token to use + :type token: str + + :param locale: The locale to use + :type locale: Locale + + :rtype: str + """ + if token in self._DATE_FORMATS: + fmt = locale.get("custom.date_formats.{}".format(token)) + if fmt is None: + fmt = self._DEFAULT_DATE_FORMATS[token] + + return self.format(dt, fmt, locale) + + if token in self._LOCALIZABLE_TOKENS: + return self._format_localizable_token(dt, token, locale) + + if token in self._TOKENS_RULES: + return self._TOKENS_RULES[token](dt) + + # Timezone + if token in ["ZZ", "Z"]: + if dt.tzinfo is None: + return "" + + separator = ":" if token == "Z" else "" + offset = dt.utcoffset() or datetime.timedelta() + minutes = offset.total_seconds() / 60 + + if minutes >= 0: + sign = "+" + else: + sign = "-" + + hour, minute = divmod(abs(int(minutes)), 60) + + return "{}{:02d}{}{:02d}".format(sign, hour, separator, minute) + + def _format_localizable_token( + self, dt, token, locale + ): # type: (pendulum.DateTime, str, Locale) -> str + """ + Formats a DateTime instance + with a given localizable token and locale. + + :param dt: The instance to format + :type dt: pendulum.DateTime + + :param token: The token to use + :type token: str + + :param locale: The locale to use + :type locale: Locale + + :rtype: str + """ + if token == "MMM": + return locale.get("translations.months.abbreviated")[dt.month] + elif token == "MMMM": + return locale.get("translations.months.wide")[dt.month] + elif token == "dd": + return locale.get("translations.days.short")[dt.day_of_week] + elif token == "ddd": + return locale.get("translations.days.abbreviated")[dt.day_of_week] + elif token == "dddd": + return locale.get("translations.days.wide")[dt.day_of_week] + elif token == "Do": + return locale.ordinalize(dt.day) + elif token == "do": + return locale.ordinalize(dt.day_of_week) + elif token == "Mo": + return locale.ordinalize(dt.month) + elif token == "Qo": + return locale.ordinalize(dt.quarter) + elif token == "wo": + return locale.ordinalize(dt.week_of_year) + elif token == "DDDo": + return locale.ordinalize(dt.day_of_year) + elif token == "A": + key = "translations.day_periods" + if dt.hour >= 12: + key += ".pm" + else: + key += ".am" + + return locale.get(key) + else: + return token + + def parse( + self, + time, # type: str + fmt, # type: str + now, # type: pendulum.DateTime + locale=None, # type: typing.Optional[str] + ): # type: (...) -> typing.Dict[str, typing.Any] + """ + Parses a time string matching a given format as a tuple. + + :param time: The timestring + :param fmt: The format + :param now: The datetime to use as "now" + :param locale: The locale to use + + :return: The parsed elements + """ + escaped_fmt = re.escape(fmt) + + tokens = self._FROM_FORMAT_RE.findall(escaped_fmt) + if not tokens: + return time + + if not locale: + locale = pendulum.get_locale() + + locale = Locale.load(locale) + + parsed = { + "year": None, + "month": None, + "day": None, + "hour": None, + "minute": None, + "second": None, + "microsecond": None, + "tz": None, + "quarter": None, + "day_of_week": None, + "day_of_year": None, + "meridiem": None, + "timestamp": None, + } + + pattern = self._FROM_FORMAT_RE.sub( + lambda m: self._replace_tokens(m.group(0), locale), escaped_fmt + ) + + if not re.search("^" + pattern + "$", time): + raise ValueError("String does not match format {}".format(fmt)) + + re.sub(pattern, lambda m: self._get_parsed_values(m, parsed, locale, now), time) + + return self._check_parsed(parsed, now) + + def _check_parsed( + self, parsed, now + ): # type: (typing.Dict[str, typing.Any], pendulum.DateTime) -> typing.Dict[str, typing.Any] + """ + Checks validity of parsed elements. + + :param parsed: The elements to parse. + + :return: The validated elements. + """ + validated = { + "year": parsed["year"], + "month": parsed["month"], + "day": parsed["day"], + "hour": parsed["hour"], + "minute": parsed["minute"], + "second": parsed["second"], + "microsecond": parsed["microsecond"], + "tz": None, + } + + # If timestamp has been specified + # we use it and don't go any further + if parsed["timestamp"] is not None: + str_us = str(parsed["timestamp"]) + if "." in str_us: + microseconds = int("{}".format(str_us.split(".")[1].ljust(6, "0"))) + else: + microseconds = 0 + + from pendulum.helpers import local_time + + time = local_time(parsed["timestamp"], 0, microseconds) + validated["year"] = time[0] + validated["month"] = time[1] + validated["day"] = time[2] + validated["hour"] = time[3] + validated["minute"] = time[4] + validated["second"] = time[5] + validated["microsecond"] = time[6] + + return validated + + if parsed["quarter"] is not None: + if validated["year"] is not None: + dt = pendulum.datetime(validated["year"], 1, 1) + else: + dt = now + + dt = dt.start_of("year") + + while dt.quarter != parsed["quarter"]: + dt = dt.add(months=3) + + validated["year"] = dt.year + validated["month"] = dt.month + validated["day"] = dt.day + + if validated["year"] is None: + validated["year"] = now.year + + if parsed["day_of_year"] is not None: + dt = pendulum.parse( + "{}-{:>03d}".format(validated["year"], parsed["day_of_year"]) + ) + + validated["month"] = dt.month + validated["day"] = dt.day + + if parsed["day_of_week"] is not None: + dt = pendulum.datetime( + validated["year"], + validated["month"] or now.month, + validated["day"] or now.day, + ) + dt = dt.start_of("week").subtract(days=1) + dt = dt.next(parsed["day_of_week"]) + validated["year"] = dt.year + validated["month"] = dt.month + validated["day"] = dt.day + + # Meridiem + if parsed["meridiem"] is not None: + # If the time is greater than 13:00:00 + # This is not valid + if validated["hour"] is None: + raise ValueError("Invalid Date") + + t = ( + validated["hour"], + validated["minute"], + validated["second"], + validated["microsecond"], + ) + if t >= (13, 0, 0, 0): + raise ValueError("Invalid date") + + pm = parsed["meridiem"] == "pm" + validated["hour"] %= 12 + if pm: + validated["hour"] += 12 + + if validated["month"] is None: + if parsed["year"] is not None: + validated["month"] = parsed["month"] or 1 + else: + validated["month"] = parsed["month"] or now.month + + if validated["day"] is None: + if parsed["year"] is not None or parsed["month"] is not None: + validated["day"] = parsed["day"] or 1 + else: + validated["day"] = parsed["day"] or now.day + + for part in ["hour", "minute", "second", "microsecond"]: + if validated[part] is None: + validated[part] = 0 + + validated["tz"] = parsed["tz"] + + return validated + + def _get_parsed_values( + self, m, parsed, locale, now + ): # type: (typing.Match[str], typing.Dict[str, typing.Any], Locale, pendulum.DateTime) -> None + for token, index in m.re.groupindex.items(): + if token in self._LOCALIZABLE_TOKENS: + self._get_parsed_locale_value(token, m.group(index), parsed, locale) + else: + self._get_parsed_value(token, m.group(index), parsed, now) + + def _get_parsed_value( + self, token, value, parsed, now + ): # type: (str, str, typing.Dict[str, typing.Any], pendulum.DateTime) -> None + parsed_token = self._PARSE_TOKENS[token](value) + + if "Y" in token: + if token == "YY": + parsed_token = now.year // 100 * 100 + parsed_token + + parsed["year"] = parsed_token + elif "Q" == token: + parsed["quarter"] = parsed_token + elif token in ["MM", "M"]: + parsed["month"] = parsed_token + elif token in ["DDDD", "DDD"]: + parsed["day_of_year"] = parsed_token + elif "D" in token: + parsed["day"] = parsed_token + elif "H" in token: + parsed["hour"] = parsed_token + elif token in ["hh", "h"]: + if parsed_token > 12: + raise ValueError("Invalid date") + + parsed["hour"] = parsed_token + elif "m" in token: + parsed["minute"] = parsed_token + elif "s" in token: + parsed["second"] = parsed_token + elif "S" in token: + parsed["microsecond"] = parsed_token + elif token in ["d", "E"]: + parsed["day_of_week"] = parsed_token + elif token in ["X", "x"]: + parsed["timestamp"] = parsed_token + elif token in ["ZZ", "Z"]: + negative = True if value.startswith("-") else False + tz = value[1:] + if ":" not in tz: + if len(tz) == 2: + tz = "{}00".format(tz) + + off_hour = tz[0:2] + off_minute = tz[2:4] + else: + off_hour, off_minute = tz.split(":") + + offset = ((int(off_hour) * 60) + int(off_minute)) * 60 + + if negative: + offset = -1 * offset + + parsed["tz"] = pendulum.timezone(offset) + elif token == "z": + # Full timezone + if value not in pendulum.timezones: + raise ValueError("Invalid date") + + parsed["tz"] = pendulum.timezone(value) + + def _get_parsed_locale_value( + self, token, value, parsed, locale + ): # type: (str, str, typing.Dict[str, typing.Any], Locale) -> None + if token == "MMMM": + unit = "month" + match = "months.wide" + elif token == "MMM": + unit = "month" + match = "months.abbreviated" + elif token == "Do": + parsed["day"] = int(re.match(r"(\d+)", value).group(1)) + + return + elif token == "dddd": + unit = "day_of_week" + match = "days.wide" + elif token == "ddd": + unit = "day_of_week" + match = "days.abbreviated" + elif token == "dd": + unit = "day_of_week" + match = "days.short" + elif token in ["a", "A"]: + valid_values = [ + locale.translation("day_periods.am"), + locale.translation("day_periods.pm"), + ] + + if token == "a": + value = value.lower() + valid_values = list(map(lambda x: x.lower(), valid_values)) + + if value not in valid_values: + raise ValueError("Invalid date") + + parsed["meridiem"] = ["am", "pm"][valid_values.index(value)] + + return + else: + raise ValueError('Invalid token "{}"'.format(token)) + + parsed[unit] = locale.match_translation(match, value) + if value is None: + raise ValueError("Invalid date") + + def _replace_tokens(self, token, locale): # type: (str, Locale) -> str + if token.startswith("[") and token.endswith("]"): + return token[1:-1] + elif token.startswith("\\"): + if len(token) == 2 and token[1] in {"[", "]"}: + return "" + + return token + elif token not in self._REGEX_TOKENS and token not in self._LOCALIZABLE_TOKENS: + raise ValueError("Unsupported token: {}".format(token)) + + if token in self._LOCALIZABLE_TOKENS: + values = self._LOCALIZABLE_TOKENS[token] + if callable(values): + candidates = values(locale) + else: + candidates = tuple( + locale.translation(self._LOCALIZABLE_TOKENS[token]).values() + ) + else: + candidates = self._REGEX_TOKENS[token] + + if not candidates: + raise ValueError("Unsupported token: {}".format(token)) + + if not isinstance(candidates, tuple): + candidates = (candidates,) + + pattern = "(?P<{}>{})".format(token, "|".join([decode(p) for p in candidates])) + + return pattern diff --git a/pendulum/helpers.py b/pendulum/helpers.py new file mode 100644 index 0000000..6e51a73 --- /dev/null +++ b/pendulum/helpers.py @@ -0,0 +1,224 @@ +from __future__ import absolute_import + +import os +import struct + +from contextlib import contextmanager +from datetime import date +from datetime import datetime +from datetime import timedelta +from math import copysign +from typing import TYPE_CHECKING +from typing import Iterator +from typing import Optional +from typing import TypeVar +from typing import overload + +import pendulum + +from .constants import DAYS_PER_MONTHS +from .formatting.difference_formatter import DifferenceFormatter +from .locales.locale import Locale + + +if TYPE_CHECKING: + # Prevent import cycles + from .period import Period + +with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1" + +_DT = TypeVar("_DT", bound=datetime) +_D = TypeVar("_D", bound=date) + +try: + if not with_extensions or struct.calcsize("P") == 4: + raise ImportError() + + from ._extensions._helpers import local_time + from ._extensions._helpers import precise_diff + from ._extensions._helpers import is_leap + from ._extensions._helpers import is_long_year + from ._extensions._helpers import week_day + from ._extensions._helpers import days_in_year + from ._extensions._helpers import timestamp +except ImportError: + from ._extensions.helpers import local_time # noqa + from ._extensions.helpers import precise_diff # noqa + from ._extensions.helpers import is_leap # noqa + from ._extensions.helpers import is_long_year # noqa + from ._extensions.helpers import week_day # noqa + from ._extensions.helpers import days_in_year # noqa + from ._extensions.helpers import timestamp # noqa + + +difference_formatter = DifferenceFormatter() + + +@overload +def add_duration( + dt, # type: _DT + years=0, # type: int + months=0, # type: int + weeks=0, # type: int + days=0, # type: int + hours=0, # type: int + minutes=0, # type: int + seconds=0, # type: int + microseconds=0, # type: int +): # type: (...) -> _DT + pass + + +@overload +def add_duration( + dt, # type: _D + years=0, # type: int + months=0, # type: int + weeks=0, # type: int + days=0, # type: int +): # type: (...) -> _D + pass + + +def add_duration( + dt, + years=0, + months=0, + weeks=0, + days=0, + hours=0, + minutes=0, + seconds=0, + microseconds=0, +): + """ + Adds a duration to a date/datetime instance. + """ + days += weeks * 7 + + if ( + isinstance(dt, date) + and not isinstance(dt, datetime) + and any([hours, minutes, seconds, microseconds]) + ): + raise RuntimeError("Time elements cannot be added to a date instance.") + + # Normalizing + if abs(microseconds) > 999999: + s = _sign(microseconds) + div, mod = divmod(microseconds * s, 1000000) + microseconds = mod * s + seconds += div * s + + if abs(seconds) > 59: + s = _sign(seconds) + div, mod = divmod(seconds * s, 60) + seconds = mod * s + minutes += div * s + + if abs(minutes) > 59: + s = _sign(minutes) + div, mod = divmod(minutes * s, 60) + minutes = mod * s + hours += div * s + + if abs(hours) > 23: + s = _sign(hours) + div, mod = divmod(hours * s, 24) + hours = mod * s + days += div * s + + if abs(months) > 11: + s = _sign(months) + div, mod = divmod(months * s, 12) + months = mod * s + years += div * s + + year = dt.year + years + month = dt.month + + if months: + month += months + if month > 12: + year += 1 + month -= 12 + elif month < 1: + year -= 1 + month += 12 + + day = min(DAYS_PER_MONTHS[int(is_leap(year))][month], dt.day) + + dt = dt.replace(year=year, month=month, day=day) + + return dt + timedelta( + days=days, + hours=hours, + minutes=minutes, + seconds=seconds, + microseconds=microseconds, + ) + + +def format_diff( + diff, is_now=True, absolute=False, locale=None +): # type: (Period, bool, bool, Optional[str]) -> str + if locale is None: + locale = get_locale() + + return difference_formatter.format(diff, is_now, absolute, locale) + + +def _sign(x): + return int(copysign(1, x)) + + +# Global helpers + + +@contextmanager +def test(mock): # type: (pendulum.DateTime) -> Iterator[None] + set_test_now(mock) + try: + yield + finally: + set_test_now() + + +def set_test_now(test_now=None): # type: (Optional[pendulum.DateTime]) -> None + pendulum._TEST_NOW = test_now + + +def get_test_now(): # type: () -> Optional[pendulum.DateTime] + return pendulum._TEST_NOW + + +def has_test_now(): # type: () -> bool + return pendulum._TEST_NOW is not None + + +def locale(name): # type: (str) -> Locale + return Locale.load(name) + + +def set_locale(name): # type: (str) -> None + locale(name) + + pendulum._LOCALE = name + + +def get_locale(): # type: () -> str + return pendulum._LOCALE + + +def week_starts_at(wday): # type: (int) -> None + if wday < pendulum.SUNDAY or wday > pendulum.SATURDAY: + raise ValueError("Invalid week day as start of week.") + + pendulum._WEEK_STARTS_AT = wday + + +def week_ends_at(wday): # type: (int) -> None + if wday < pendulum.SUNDAY or wday > pendulum.SATURDAY: + raise ValueError("Invalid week day as start of week.") + + pendulum._WEEK_ENDS_AT = wday diff --git a/pendulum/locales/__init__.py b/pendulum/locales/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/__init__.py diff --git a/pendulum/locales/da/__init__.py b/pendulum/locales/da/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/da/__init__.py diff --git a/pendulum/locales/da/custom.py b/pendulum/locales/da/custom.py new file mode 100644 index 0000000..eaf3655 --- /dev/null +++ b/pendulum/locales/da/custom.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +da custom locale file. +""" + +translations = { + # Relative time + "after": "{0} efter", + "before": "{0} før", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd [d.] D. MMMM YYYY HH:mm", + "LLL": "D. MMMM YYYY HH:mm", + "LL": "D. MMMM YYYY", + "L": "DD/MM/YYYY", + }, +} diff --git a/pendulum/locales/da/locale.py b/pendulum/locales/da/locale.py new file mode 100644 index 0000000..addee68 --- /dev/null +++ b/pendulum/locales/da/locale.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +da locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ( + (n == n and ((n == 1))) + or ((not (0 == 0 and ((0 == 0)))) and (n == n and ((n == 0) or (n == 1)))) + ) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "søn.", + 1: "man.", + 2: "tir.", + 3: "ons.", + 4: "tor.", + 5: "fre.", + 6: "lør.", + }, + "narrow": {0: "S", 1: "M", 2: "T", 3: "O", 4: "T", 5: "F", 6: "L"}, + "short": {0: "sø", 1: "ma", 2: "ti", 3: "on", 4: "to", 5: "fr", 6: "lø"}, + "wide": { + 0: "søndag", + 1: "mandag", + 2: "tirsdag", + 3: "onsdag", + 4: "torsdag", + 5: "fredag", + 6: "lørdag", + }, + }, + "months": { + "abbreviated": { + 1: "jan.", + 2: "feb.", + 3: "mar.", + 4: "apr.", + 5: "maj", + 6: "jun.", + 7: "jul.", + 8: "aug.", + 9: "sep.", + 10: "okt.", + 11: "nov.", + 12: "dec.", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "januar", + 2: "februar", + 3: "marts", + 4: "april", + 5: "maj", + 6: "juni", + 7: "juli", + 8: "august", + 9: "september", + 10: "oktober", + 11: "november", + 12: "december", + }, + }, + "units": { + "year": {"one": "{0} år", "other": "{0} år"}, + "month": {"one": "{0} måned", "other": "{0} måneder"}, + "week": {"one": "{0} uge", "other": "{0} uger"}, + "day": {"one": "{0} dag", "other": "{0} dage"}, + "hour": {"one": "{0} time", "other": "{0} timer"}, + "minute": {"one": "{0} minut", "other": "{0} minutter"}, + "second": {"one": "{0} sekund", "other": "{0} sekunder"}, + "microsecond": {"one": "{0} mikrosekund", "other": "{0} mikrosekunder"}, + }, + "relative": { + "year": { + "future": {"other": "om {0} år", "one": "om {0} år"}, + "past": {"other": "for {0} år siden", "one": "for {0} år siden"}, + }, + "month": { + "future": {"other": "om {0} måneder", "one": "om {0} måned"}, + "past": { + "other": "for {0} måneder siden", + "one": "for {0} måned siden", + }, + }, + "week": { + "future": {"other": "om {0} uger", "one": "om {0} uge"}, + "past": {"other": "for {0} uger siden", "one": "for {0} uge siden"}, + }, + "day": { + "future": {"other": "om {0} dage", "one": "om {0} dag"}, + "past": {"other": "for {0} dage siden", "one": "for {0} dag siden"}, + }, + "hour": { + "future": {"other": "om {0} timer", "one": "om {0} time"}, + "past": {"other": "for {0} timer siden", "one": "for {0} time siden"}, + }, + "minute": { + "future": {"other": "om {0} minutter", "one": "om {0} minut"}, + "past": { + "other": "for {0} minutter siden", + "one": "for {0} minut siden", + }, + }, + "second": { + "future": {"other": "om {0} sekunder", "one": "om {0} sekund"}, + "past": { + "other": "for {0} sekunder siden", + "one": "for {0} sekund siden", + }, + }, + }, + "day_periods": { + "midnight": "midnat", + "am": "AM", + "pm": "PM", + "morning1": "om morgenen", + "morning2": "om formiddagen", + "afternoon1": "om eftermiddagen", + "evening1": "om aftenen", + "night1": "om natten", + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/de/__init__.py b/pendulum/locales/de/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/de/__init__.py diff --git a/pendulum/locales/de/custom.py b/pendulum/locales/de/custom.py new file mode 100644 index 0000000..45fd591 --- /dev/null +++ b/pendulum/locales/de/custom.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +de custom locale file. +""" + +translations = { + # Relative time + "after": "{0} später", + "before": "{0} zuvor", + "units_relative": { + "year": { + "future": {"one": "{0} Jahr", "other": "{0} Jahren"}, + "past": {"one": "{0} Jahr", "other": "{0} Jahren"}, + }, + "month": { + "future": {"one": "{0} Monat", "other": "{0} Monaten"}, + "past": {"one": "{0} Monat", "other": "{0} Monaten"}, + }, + "week": { + "future": {"one": "{0} Woche", "other": "{0} Wochen"}, + "past": {"one": "{0} Woche", "other": "{0} Wochen"}, + }, + "day": { + "future": {"one": "{0} Tag", "other": "{0} Tagen"}, + "past": {"one": "{0} Tag", "other": "{0} Tagen"}, + }, + }, + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd, D. MMMM YYYY HH:mm", + "LLL": "D. MMMM YYYY HH:mm", + "LL": "D. MMMM YYYY", + "L": "DD.MM.YYYY", + }, +} diff --git a/pendulum/locales/de/locale.py b/pendulum/locales/de/locale.py new file mode 100644 index 0000000..9a9ec9f --- /dev/null +++ b/pendulum/locales/de/locale.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +de locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and ((n == 1))) and (0 == 0 and ((0 == 0)))) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "So.", + 1: "Mo.", + 2: "Di.", + 3: "Mi.", + 4: "Do.", + 5: "Fr.", + 6: "Sa.", + }, + "narrow": {0: "S", 1: "M", 2: "D", 3: "M", 4: "D", 5: "F", 6: "S"}, + "short": { + 0: "So.", + 1: "Mo.", + 2: "Di.", + 3: "Mi.", + 4: "Do.", + 5: "Fr.", + 6: "Sa.", + }, + "wide": { + 0: "Sonntag", + 1: "Montag", + 2: "Dienstag", + 3: "Mittwoch", + 4: "Donnerstag", + 5: "Freitag", + 6: "Samstag", + }, + }, + "months": { + "abbreviated": { + 1: "Jan.", + 2: "Feb.", + 3: "März", + 4: "Apr.", + 5: "Mai", + 6: "Juni", + 7: "Juli", + 8: "Aug.", + 9: "Sep.", + 10: "Okt.", + 11: "Nov.", + 12: "Dez.", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "Januar", + 2: "Februar", + 3: "März", + 4: "April", + 5: "Mai", + 6: "Juni", + 7: "Juli", + 8: "August", + 9: "September", + 10: "Oktober", + 11: "November", + 12: "Dezember", + }, + }, + "units": { + "year": {"one": "{0} Jahr", "other": "{0} Jahre"}, + "month": {"one": "{0} Monat", "other": "{0} Monate"}, + "week": {"one": "{0} Woche", "other": "{0} Wochen"}, + "day": {"one": "{0} Tag", "other": "{0} Tage"}, + "hour": {"one": "{0} Stunde", "other": "{0} Stunden"}, + "minute": {"one": "{0} Minute", "other": "{0} Minuten"}, + "second": {"one": "{0} Sekunde", "other": "{0} Sekunden"}, + "microsecond": {"one": "{0} Mikrosekunde", "other": "{0} Mikrosekunden"}, + }, + "relative": { + "year": { + "future": {"other": "in {0} Jahren", "one": "in {0} Jahr"}, + "past": {"other": "vor {0} Jahren", "one": "vor {0} Jahr"}, + }, + "month": { + "future": {"other": "in {0} Monaten", "one": "in {0} Monat"}, + "past": {"other": "vor {0} Monaten", "one": "vor {0} Monat"}, + }, + "week": { + "future": {"other": "in {0} Wochen", "one": "in {0} Woche"}, + "past": {"other": "vor {0} Wochen", "one": "vor {0} Woche"}, + }, + "day": { + "future": {"other": "in {0} Tagen", "one": "in {0} Tag"}, + "past": {"other": "vor {0} Tagen", "one": "vor {0} Tag"}, + }, + "hour": { + "future": {"other": "in {0} Stunden", "one": "in {0} Stunde"}, + "past": {"other": "vor {0} Stunden", "one": "vor {0} Stunde"}, + }, + "minute": { + "future": {"other": "in {0} Minuten", "one": "in {0} Minute"}, + "past": {"other": "vor {0} Minuten", "one": "vor {0} Minute"}, + }, + "second": { + "future": {"other": "in {0} Sekunden", "one": "in {0} Sekunde"}, + "past": {"other": "vor {0} Sekunden", "one": "vor {0} Sekunde"}, + }, + }, + "day_periods": { + "midnight": "Mitternacht", + "am": "vorm.", + "pm": "nachm.", + "morning1": "morgens", + "morning2": "vormittags", + "afternoon1": "mittags", + "afternoon2": "nachmittags", + "evening1": "abends", + "night1": "nachts", + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/en/__init__.py b/pendulum/locales/en/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/en/__init__.py diff --git a/pendulum/locales/en/custom.py b/pendulum/locales/en/custom.py new file mode 100644 index 0000000..9e631a2 --- /dev/null +++ b/pendulum/locales/en/custom.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +en custom locale file. +""" + +translations = { + "units": {"few_second": "a few seconds"}, + # Relative time + "ago": "{} ago", + "from_now": "in {}", + "after": "{0} after", + "before": "{0} before", + # Ordinals + "ordinal": {"one": "st", "two": "nd", "few": "rd", "other": "th"}, + # Date formats + "date_formats": { + "LTS": "h:mm:ss A", + "LT": "h:mm A", + "L": "MM/DD/YYYY", + "LL": "MMMM D, YYYY", + "LLL": "MMMM D, YYYY h:mm A", + "LLLL": "dddd, MMMM D, YYYY h:mm A", + }, +} diff --git a/pendulum/locales/en/locale.py b/pendulum/locales/en/locale.py new file mode 100644 index 0000000..917a4ce --- /dev/null +++ b/pendulum/locales/en/locale.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +en locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and ((n == 1))) and (0 == 0 and ((0 == 0)))) + else "other", + "ordinal": lambda n: "few" + if ( + ((n % 10) == (n % 10) and (((n % 10) == 3))) + and (not ((n % 100) == (n % 100) and (((n % 100) == 13)))) + ) + else "one" + if ( + ((n % 10) == (n % 10) and (((n % 10) == 1))) + and (not ((n % 100) == (n % 100) and (((n % 100) == 11)))) + ) + else "two" + if ( + ((n % 10) == (n % 10) and (((n % 10) == 2))) + and (not ((n % 100) == (n % 100) and (((n % 100) == 12)))) + ) + else "other", + "translations": { + "days": { + "abbreviated": { + 0: "Sun", + 1: "Mon", + 2: "Tue", + 3: "Wed", + 4: "Thu", + 5: "Fri", + 6: "Sat", + }, + "narrow": {0: "S", 1: "M", 2: "T", 3: "W", 4: "T", 5: "F", 6: "S"}, + "short": {0: "Su", 1: "Mo", 2: "Tu", 3: "We", 4: "Th", 5: "Fr", 6: "Sa"}, + "wide": { + 0: "Sunday", + 1: "Monday", + 2: "Tuesday", + 3: "Wednesday", + 4: "Thursday", + 5: "Friday", + 6: "Saturday", + }, + }, + "months": { + "abbreviated": { + 1: "Jan", + 2: "Feb", + 3: "Mar", + 4: "Apr", + 5: "May", + 6: "Jun", + 7: "Jul", + 8: "Aug", + 9: "Sep", + 10: "Oct", + 11: "Nov", + 12: "Dec", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "January", + 2: "February", + 3: "March", + 4: "April", + 5: "May", + 6: "June", + 7: "July", + 8: "August", + 9: "September", + 10: "October", + 11: "November", + 12: "December", + }, + }, + "units": { + "year": {"one": "{0} year", "other": "{0} years"}, + "month": {"one": "{0} month", "other": "{0} months"}, + "week": {"one": "{0} week", "other": "{0} weeks"}, + "day": {"one": "{0} day", "other": "{0} days"}, + "hour": {"one": "{0} hour", "other": "{0} hours"}, + "minute": {"one": "{0} minute", "other": "{0} minutes"}, + "second": {"one": "{0} second", "other": "{0} seconds"}, + "microsecond": {"one": "{0} microsecond", "other": "{0} microseconds"}, + }, + "relative": { + "year": { + "future": {"other": "in {0} years", "one": "in {0} year"}, + "past": {"other": "{0} years ago", "one": "{0} year ago"}, + }, + "month": { + "future": {"other": "in {0} months", "one": "in {0} month"}, + "past": {"other": "{0} months ago", "one": "{0} month ago"}, + }, + "week": { + "future": {"other": "in {0} weeks", "one": "in {0} week"}, + "past": {"other": "{0} weeks ago", "one": "{0} week ago"}, + }, + "day": { + "future": {"other": "in {0} days", "one": "in {0} day"}, + "past": {"other": "{0} days ago", "one": "{0} day ago"}, + }, + "hour": { + "future": {"other": "in {0} hours", "one": "in {0} hour"}, + "past": {"other": "{0} hours ago", "one": "{0} hour ago"}, + }, + "minute": { + "future": {"other": "in {0} minutes", "one": "in {0} minute"}, + "past": {"other": "{0} minutes ago", "one": "{0} minute ago"}, + }, + "second": { + "future": {"other": "in {0} seconds", "one": "in {0} second"}, + "past": {"other": "{0} seconds ago", "one": "{0} second ago"}, + }, + }, + "day_periods": { + "midnight": "midnight", + "am": "AM", + "noon": "noon", + "pm": "PM", + "morning1": "in the morning", + "afternoon1": "in the afternoon", + "evening1": "in the evening", + "night1": "at night", + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/es/__init__.py b/pendulum/locales/es/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/es/__init__.py diff --git a/pendulum/locales/es/custom.py b/pendulum/locales/es/custom.py new file mode 100644 index 0000000..18585e0 --- /dev/null +++ b/pendulum/locales/es/custom.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +es custom locale file. +""" + +translations = { + "units": {"few_second": "unos segundos"}, + # Relative time + "ago": "hace {0}", + "from_now": "dentro de {0}", + "after": "{0} después", + "before": "{0} antes", + # Ordinals + "ordinal": {"other": "º"}, + # Date formats + "date_formats": { + "LTS": "H:mm:ss", + "LT": "H:mm", + "LLLL": "dddd, D [de] MMMM [de] YYYY H:mm", + "LLL": "D [de] MMMM [de] YYYY H:mm", + "LL": "D [de] MMMM [de] YYYY", + "L": "DD/MM/YYYY", + }, +} diff --git a/pendulum/locales/es/locale.py b/pendulum/locales/es/locale.py new file mode 100644 index 0000000..2f6266b --- /dev/null +++ b/pendulum/locales/es/locale.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +es locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" if (n == n and ((n == 1))) else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "dom.", + 1: "lun.", + 2: "mar.", + 3: "mié.", + 4: "jue.", + 5: "vie.", + 6: "sáb.", + }, + "narrow": {0: "D", 1: "L", 2: "M", 3: "X", 4: "J", 5: "V", 6: "S"}, + "short": {0: "DO", 1: "LU", 2: "MA", 3: "MI", 4: "JU", 5: "VI", 6: "SA"}, + "wide": { + 0: "domingo", + 1: "lunes", + 2: "martes", + 3: "miércoles", + 4: "jueves", + 5: "viernes", + 6: "sábado", + }, + }, + "months": { + "abbreviated": { + 1: "ene.", + 2: "feb.", + 3: "mar.", + 4: "abr.", + 5: "may.", + 6: "jun.", + 7: "jul.", + 8: "ago.", + 9: "sept.", + 10: "oct.", + 11: "nov.", + 12: "dic.", + }, + "narrow": { + 1: "E", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "enero", + 2: "febrero", + 3: "marzo", + 4: "abril", + 5: "mayo", + 6: "junio", + 7: "julio", + 8: "agosto", + 9: "septiembre", + 10: "octubre", + 11: "noviembre", + 12: "diciembre", + }, + }, + "units": { + "year": {"one": "{0} año", "other": "{0} años"}, + "month": {"one": "{0} mes", "other": "{0} meses"}, + "week": {"one": "{0} semana", "other": "{0} semanas"}, + "day": {"one": "{0} día", "other": "{0} días"}, + "hour": {"one": "{0} hora", "other": "{0} horas"}, + "minute": {"one": "{0} minuto", "other": "{0} minutos"}, + "second": {"one": "{0} segundo", "other": "{0} segundos"}, + "microsecond": {"one": "{0} microsegundo", "other": "{0} microsegundos"}, + }, + "relative": { + "year": { + "future": {"other": "dentro de {0} años", "one": "dentro de {0} año"}, + "past": {"other": "hace {0} años", "one": "hace {0} año"}, + }, + "month": { + "future": {"other": "dentro de {0} meses", "one": "dentro de {0} mes"}, + "past": {"other": "hace {0} meses", "one": "hace {0} mes"}, + }, + "week": { + "future": { + "other": "dentro de {0} semanas", + "one": "dentro de {0} semana", + }, + "past": {"other": "hace {0} semanas", "one": "hace {0} semana"}, + }, + "day": { + "future": {"other": "dentro de {0} días", "one": "dentro de {0} día"}, + "past": {"other": "hace {0} días", "one": "hace {0} día"}, + }, + "hour": { + "future": {"other": "dentro de {0} horas", "one": "dentro de {0} hora"}, + "past": {"other": "hace {0} horas", "one": "hace {0} hora"}, + }, + "minute": { + "future": { + "other": "dentro de {0} minutos", + "one": "dentro de {0} minuto", + }, + "past": {"other": "hace {0} minutos", "one": "hace {0} minuto"}, + }, + "second": { + "future": { + "other": "dentro de {0} segundos", + "one": "dentro de {0} segundo", + }, + "past": {"other": "hace {0} segundos", "one": "hace {0} segundo"}, + }, + }, + "day_periods": { + "am": "a. m.", + "noon": "del mediodía", + "pm": "p. m.", + "morning1": "de la madrugada", + "morning2": "de la mañana", + "evening1": "de la tarde", + "night1": "de la noche", + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/fa/__init__.py b/pendulum/locales/fa/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/fa/__init__.py diff --git a/pendulum/locales/fa/custom.py b/pendulum/locales/fa/custom.py new file mode 100644 index 0000000..9cc84d3 --- /dev/null +++ b/pendulum/locales/fa/custom.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +fa custom locale file. +""" + +translations = { + # Relative time + "after": "{0} پس از", + "before": "{0} پیش از", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd, D MMMM YYYY HH:mm", + "LLL": "D MMMM YYYY HH:mm", + "LL": "D MMMM YYYY", + "L": "DD/MM/YYYY", + }, +} diff --git a/pendulum/locales/fa/locale.py b/pendulum/locales/fa/locale.py new file mode 100644 index 0000000..4c5719d --- /dev/null +++ b/pendulum/locales/fa/locale.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +fa locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and ((n == 0))) or (n == n and ((n == 1)))) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "یکشنبه", + 1: "دوشنبه", + 2: "سه\u200cشنبه", + 3: "چهارشنبه", + 4: "پنجشنبه", + 5: "جمعه", + 6: "شنبه", + }, + "narrow": {0: "ی", 1: "د", 2: "س", 3: "چ", 4: "پ", 5: "ج", 6: "ش"}, + "short": {0: "۱ش", 1: "۲ش", 2: "۳ش", 3: "۴ش", 4: "۵ش", 5: "ج", 6: "ش"}, + "wide": { + 0: "یکشنبه", + 1: "دوشنبه", + 2: "سه\u200cشنبه", + 3: "چهارشنبه", + 4: "پنجشنبه", + 5: "جمعه", + 6: "شنبه", + }, + }, + "months": { + "abbreviated": { + 1: "ژانویهٔ", + 2: "فوریهٔ", + 3: "مارس", + 4: "آوریل", + 5: "مهٔ", + 6: "ژوئن", + 7: "ژوئیهٔ", + 8: "اوت", + 9: "سپتامبر", + 10: "اکتبر", + 11: "نوامبر", + 12: "دسامبر", + }, + "narrow": { + 1: "ژ", + 2: "ف", + 3: "م", + 4: "آ", + 5: "م", + 6: "ژ", + 7: "ژ", + 8: "ا", + 9: "س", + 10: "ا", + 11: "ن", + 12: "د", + }, + "wide": { + 1: "ژانویهٔ", + 2: "فوریهٔ", + 3: "مارس", + 4: "آوریل", + 5: "مهٔ", + 6: "ژوئن", + 7: "ژوئیهٔ", + 8: "اوت", + 9: "سپتامبر", + 10: "اکتبر", + 11: "نوامبر", + 12: "دسامبر", + }, + }, + "units": { + "year": {"one": "{0} سال", "other": "{0} سال"}, + "month": {"one": "{0} ماه", "other": "{0} ماه"}, + "week": {"one": "{0} هفته", "other": "{0} هفته"}, + "day": {"one": "{0} روز", "other": "{0} روز"}, + "hour": {"one": "{0} ساعت", "other": "{0} ساعت"}, + "minute": {"one": "{0} دقیقه", "other": "{0} دقیقه"}, + "second": {"one": "{0} ثانیه", "other": "{0} ثانیه"}, + "microsecond": {"one": "{0} میکروثانیه", "other": "{0} میکروثانیه"}, + }, + "relative": { + "year": { + "future": {"other": "{0} سال بعد", "one": "{0} سال بعد"}, + "past": {"other": "{0} سال پیش", "one": "{0} سال پیش"}, + }, + "month": { + "future": {"other": "{0} ماه بعد", "one": "{0} ماه بعد"}, + "past": {"other": "{0} ماه پیش", "one": "{0} ماه پیش"}, + }, + "week": { + "future": {"other": "{0} هفته بعد", "one": "{0} هفته بعد"}, + "past": {"other": "{0} هفته پیش", "one": "{0} هفته پیش"}, + }, + "day": { + "future": {"other": "{0} روز بعد", "one": "{0} روز بعد"}, + "past": {"other": "{0} روز پیش", "one": "{0} روز پیش"}, + }, + "hour": { + "future": {"other": "{0} ساعت بعد", "one": "{0} ساعت بعد"}, + "past": {"other": "{0} ساعت پیش", "one": "{0} ساعت پیش"}, + }, + "minute": { + "future": {"other": "{0} دقیقه بعد", "one": "{0} دقیقه بعد"}, + "past": {"other": "{0} دقیقه پیش", "one": "{0} دقیقه پیش"}, + }, + "second": { + "future": {"other": "{0} ثانیه بعد", "one": "{0} ثانیه بعد"}, + "past": {"other": "{0} ثانیه پیش", "one": "{0} ثانیه پیش"}, + }, + }, + "day_periods": { + "midnight": "نیمه\u200cشب", + "am": "قبل\u200cازظهر", + "noon": "ظهر", + "pm": "بعدازظهر", + "morning1": "صبح", + "afternoon1": "عصر", + "evening1": "عصر", + "night1": "شب", + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/fo/__init__.py b/pendulum/locales/fo/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/fo/__init__.py diff --git a/pendulum/locales/fo/custom.py b/pendulum/locales/fo/custom.py new file mode 100644 index 0000000..31f7f45 --- /dev/null +++ b/pendulum/locales/fo/custom.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +fo custom locale file. +""" + +translations = { + # Relative time + "after": "{0} aftaná", + "before": "{0} áðrenn", + # Ordinals + "ordinal": {"other": "."}, + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd D. MMMM, YYYY HH:mm", + "LLL": "D MMMM YYYY HH:mm", + "LL": "D MMMM YYYY", + "L": "DD/MM/YYYY", + }, +} diff --git a/pendulum/locales/fo/locale.py b/pendulum/locales/fo/locale.py new file mode 100644 index 0000000..8c87580 --- /dev/null +++ b/pendulum/locales/fo/locale.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +fo locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" if (n == n and ((n == 1))) else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "sun.", + 1: "mán.", + 2: "týs.", + 3: "mik.", + 4: "hós.", + 5: "frí.", + 6: "ley.", + }, + "narrow": {0: "S", 1: "M", 2: "T", 3: "M", 4: "H", 5: "F", 6: "L"}, + "short": { + 0: "su.", + 1: "má.", + 2: "tý.", + 3: "mi.", + 4: "hó.", + 5: "fr.", + 6: "le.", + }, + "wide": { + 0: "sunnudagur", + 1: "mánadagur", + 2: "týsdagur", + 3: "mikudagur", + 4: "hósdagur", + 5: "fríggjadagur", + 6: "leygardagur", + }, + }, + "months": { + "abbreviated": { + 1: "jan.", + 2: "feb.", + 3: "mar.", + 4: "apr.", + 5: "mai", + 6: "jun.", + 7: "jul.", + 8: "aug.", + 9: "sep.", + 10: "okt.", + 11: "nov.", + 12: "des.", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "januar", + 2: "februar", + 3: "mars", + 4: "apríl", + 5: "mai", + 6: "juni", + 7: "juli", + 8: "august", + 9: "september", + 10: "oktober", + 11: "november", + 12: "desember", + }, + }, + "units": { + "year": {"one": "{0} ár", "other": "{0} ár"}, + "month": {"one": "{0} mánaður", "other": "{0} mánaðir"}, + "week": {"one": "{0} vika", "other": "{0} vikur"}, + "day": {"one": "{0} dagur", "other": "{0} dagar"}, + "hour": {"one": "{0} tími", "other": "{0} tímar"}, + "minute": {"one": "{0} minuttur", "other": "{0} minuttir"}, + "second": {"one": "{0} sekund", "other": "{0} sekundir"}, + "microsecond": {"one": "{0} mikrosekund", "other": "{0} mikrosekundir"}, + }, + "relative": { + "year": { + "future": {"other": "um {0} ár", "one": "um {0} ár"}, + "past": {"other": "{0} ár síðan", "one": "{0} ár síðan"}, + }, + "month": { + "future": {"other": "um {0} mánaðir", "one": "um {0} mánað"}, + "past": {"other": "{0} mánaðir síðan", "one": "{0} mánað síðan"}, + }, + "week": { + "future": {"other": "um {0} vikur", "one": "um {0} viku"}, + "past": {"other": "{0} vikur síðan", "one": "{0} vika síðan"}, + }, + "day": { + "future": {"other": "um {0} dagar", "one": "um {0} dag"}, + "past": {"other": "{0} dagar síðan", "one": "{0} dagur síðan"}, + }, + "hour": { + "future": {"other": "um {0} tímar", "one": "um {0} tíma"}, + "past": {"other": "{0} tímar síðan", "one": "{0} tími síðan"}, + }, + "minute": { + "future": {"other": "um {0} minuttir", "one": "um {0} minutt"}, + "past": {"other": "{0} minuttir síðan", "one": "{0} minutt síðan"}, + }, + "second": { + "future": {"other": "um {0} sekund", "one": "um {0} sekund"}, + "past": {"other": "{0} sekund síðan", "one": "{0} sekund síðan"}, + }, + }, + "day_periods": {"am": "AM", "pm": "PM"}, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/fr/__init__.py b/pendulum/locales/fr/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/pendulum/locales/fr/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/pendulum/locales/fr/custom.py b/pendulum/locales/fr/custom.py new file mode 100644 index 0000000..14d480f --- /dev/null +++ b/pendulum/locales/fr/custom.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +fr custom locale file. +""" + +translations = { + "units": {"few_second": "quelques secondes"}, + # Relative Time + "ago": "il y a {0}", + "from_now": "dans {0}", + "after": "{0} après", + "before": "{0} avant", + # Ordinals + "ordinal": {"one": "er", "other": "e"}, + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd D MMMM YYYY HH:mm", + "LLL": "D MMMM YYYY HH:mm", + "LL": "D MMMM YYYY", + "L": "DD/MM/YYYY", + }, +} diff --git a/pendulum/locales/fr/locale.py b/pendulum/locales/fr/locale.py new file mode 100644 index 0000000..c884ce9 --- /dev/null +++ b/pendulum/locales/fr/locale.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +fr locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" if (n == n and ((n == 0) or (n == 1))) else "other", + "ordinal": lambda n: "one" if (n == n and ((n == 1))) else "other", + "translations": { + "days": { + "abbreviated": { + 0: "dim.", + 1: "lun.", + 2: "mar.", + 3: "mer.", + 4: "jeu.", + 5: "ven.", + 6: "sam.", + }, + "narrow": {0: "D", 1: "L", 2: "M", 3: "M", 4: "J", 5: "V", 6: "S"}, + "short": {0: "di", 1: "lu", 2: "ma", 3: "me", 4: "je", 5: "ve", 6: "sa"}, + "wide": { + 0: "dimanche", + 1: "lundi", + 2: "mardi", + 3: "mercredi", + 4: "jeudi", + 5: "vendredi", + 6: "samedi", + }, + }, + "months": { + "abbreviated": { + 1: "janv.", + 2: "févr.", + 3: "mars", + 4: "avr.", + 5: "mai", + 6: "juin", + 7: "juil.", + 8: "août", + 9: "sept.", + 10: "oct.", + 11: "nov.", + 12: "déc.", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "janvier", + 2: "février", + 3: "mars", + 4: "avril", + 5: "mai", + 6: "juin", + 7: "juillet", + 8: "août", + 9: "septembre", + 10: "octobre", + 11: "novembre", + 12: "décembre", + }, + }, + "units": { + "year": {"one": "{0} an", "other": "{0} ans"}, + "month": {"one": "{0} mois", "other": "{0} mois"}, + "week": {"one": "{0} semaine", "other": "{0} semaines"}, + "day": {"one": "{0} jour", "other": "{0} jours"}, + "hour": {"one": "{0} heure", "other": "{0} heures"}, + "minute": {"one": "{0} minute", "other": "{0} minutes"}, + "second": {"one": "{0} seconde", "other": "{0} secondes"}, + "microsecond": {"one": "{0} microseconde", "other": "{0} microsecondes"}, + }, + "relative": { + "year": { + "future": {"other": "dans {0} ans", "one": "dans {0} an"}, + "past": {"other": "il y a {0} ans", "one": "il y a {0} an"}, + }, + "month": { + "future": {"other": "dans {0} mois", "one": "dans {0} mois"}, + "past": {"other": "il y a {0} mois", "one": "il y a {0} mois"}, + }, + "week": { + "future": {"other": "dans {0} semaines", "one": "dans {0} semaine"}, + "past": {"other": "il y a {0} semaines", "one": "il y a {0} semaine"}, + }, + "day": { + "future": {"other": "dans {0} jours", "one": "dans {0} jour"}, + "past": {"other": "il y a {0} jours", "one": "il y a {0} jour"}, + }, + "hour": { + "future": {"other": "dans {0} heures", "one": "dans {0} heure"}, + "past": {"other": "il y a {0} heures", "one": "il y a {0} heure"}, + }, + "minute": { + "future": {"other": "dans {0} minutes", "one": "dans {0} minute"}, + "past": {"other": "il y a {0} minutes", "one": "il y a {0} minute"}, + }, + "second": { + "future": {"other": "dans {0} secondes", "one": "dans {0} seconde"}, + "past": {"other": "il y a {0} secondes", "one": "il y a {0} seconde"}, + }, + }, + "day_periods": { + "midnight": "minuit", + "am": "AM", + "noon": "midi", + "pm": "PM", + "morning1": "du matin", + "afternoon1": "de l’après-midi", + "evening1": "du soir", + "night1": "de nuit", + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/id/__init__.py b/pendulum/locales/id/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/id/__init__.py diff --git a/pendulum/locales/id/custom.py b/pendulum/locales/id/custom.py new file mode 100644 index 0000000..8abd474 --- /dev/null +++ b/pendulum/locales/id/custom.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +id custom locale file. +""" + +translations = { + "units": {"few_second": "beberapa detik"}, + "ago": "{} yang lalu", + "from_now": "dalam {}", + "after": "{0} kemudian", + "before": "{0} yang lalu", + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd [d.] D. MMMM YYYY HH:mm", + "LLL": "D. MMMM YYYY HH:mm", + "LL": "D. MMMM YYYY", + "L": "DD/MM/YYYY", + }, +} diff --git a/pendulum/locales/id/locale.py b/pendulum/locales/id/locale.py new file mode 100644 index 0000000..44ee697 --- /dev/null +++ b/pendulum/locales/id/locale.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +id locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "Min", + 1: "Sen", + 2: "Sel", + 3: "Rab", + 4: "Kam", + 5: "Jum", + 6: "Sab", + }, + "narrow": {0: "M", 1: "S", 2: "S", 3: "R", 4: "K", 5: "J", 6: "S"}, + "short": { + 0: "Min", + 1: "Sen", + 2: "Sel", + 3: "Rab", + 4: "Kam", + 5: "Jum", + 6: "Sab", + }, + "wide": { + 0: "Minggu", + 1: "Senin", + 2: "Selasa", + 3: "Rabu", + 4: "Kamis", + 5: "Jumat", + 6: "Sabtu", + }, + }, + "months": { + "abbreviated": { + 1: "Jan", + 2: "Feb", + 3: "Mar", + 4: "Apr", + 5: "Mei", + 6: "Jun", + 7: "Jul", + 8: "Agt", + 9: "Sep", + 10: "Okt", + 11: "Nov", + 12: "Des", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "Januari", + 2: "Februari", + 3: "Maret", + 4: "April", + 5: "Mei", + 6: "Juni", + 7: "Juli", + 8: "Agustus", + 9: "September", + 10: "Oktober", + 11: "November", + 12: "Desember", + }, + }, + "units": { + "year": {"other": "{0} tahun"}, + "month": {"other": "{0} bulan"}, + "week": {"other": "{0} minggu"}, + "day": {"other": "{0} hari"}, + "hour": {"other": "{0} jam"}, + "minute": {"other": "{0} menit"}, + "second": {"other": "{0} detik"}, + "microsecond": {"other": "{0} mikrodetik"}, + }, + "relative": { + "year": { + "future": {"other": "dalam {0} tahun"}, + "past": {"other": "{0} tahun yang lalu"}, + }, + "month": { + "future": {"other": "dalam {0} bulan"}, + "past": {"other": "{0} bulan yang lalu"}, + }, + "week": { + "future": {"other": "dalam {0} minggu"}, + "past": {"other": "{0} minggu yang lalu"}, + }, + "day": { + "future": {"other": "dalam {0} hari"}, + "past": {"other": "{0} hari yang lalu"}, + }, + "hour": { + "future": {"other": "dalam {0} jam"}, + "past": {"other": "{0} jam yang lalu"}, + }, + "minute": { + "future": {"other": "dalam {0} menit"}, + "past": {"other": "{0} menit yang lalu"}, + }, + "second": { + "future": {"other": "dalam {0} detik"}, + "past": {"other": "{0} detik yang lalu"}, + }, + }, + "day_periods": { + "midnight": "tengah malam", + "am": "AM", + "noon": "tengah hari", + "pm": "PM", + "morning1": "pagi", + "afternoon1": "siang", + "evening1": "sore", + "night1": "malam", + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/it/__init__.py b/pendulum/locales/it/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/it/__init__.py diff --git a/pendulum/locales/it/custom.py b/pendulum/locales/it/custom.py new file mode 100644 index 0000000..6f3963e --- /dev/null +++ b/pendulum/locales/it/custom.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +""" +it custom locale file. +""" + +from __future__ import unicode_literals + + +translations = { + "units": {"few_second": "alcuni secondi"}, + # Relative Time + "ago": "{0} fa", + "from_now": "in {0}", + "after": "{0} dopo", + "before": "{0} prima", + # Ordinals + "ordinal": {"other": "°"}, + # Date formats + "date_formats": { + "LTS": "H:mm:ss", + "LT": "H:mm", + "L": "DD/MM/YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY [alle] H:mm", + "LLLL": "dddd, D MMMM YYYY [alle] H:mm", + }, +} diff --git a/pendulum/locales/it/locale.py b/pendulum/locales/it/locale.py new file mode 100644 index 0000000..920a778 --- /dev/null +++ b/pendulum/locales/it/locale.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +it locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and ((n == 1))) and (0 == 0 and ((0 == 0)))) + else "other", + "ordinal": lambda n: "many" + if (n == n and ((n == 11) or (n == 8) or (n == 80) or (n == 800))) + else "other", + "translations": { + "days": { + "abbreviated": { + 0: "dom", + 1: "lun", + 2: "mar", + 3: "mer", + 4: "gio", + 5: "ven", + 6: "sab", + }, + "narrow": {0: "D", 1: "L", 2: "M", 3: "M", 4: "G", 5: "V", 6: "S"}, + "short": { + 0: "dom", + 1: "lun", + 2: "mar", + 3: "mer", + 4: "gio", + 5: "ven", + 6: "sab", + }, + "wide": { + 0: "domenica", + 1: "lunedì", + 2: "martedì", + 3: "mercoledì", + 4: "giovedì", + 5: "venerdì", + 6: "sabato", + }, + }, + "months": { + "abbreviated": { + 1: "gen", + 2: "feb", + 3: "mar", + 4: "apr", + 5: "mag", + 6: "giu", + 7: "lug", + 8: "ago", + 9: "set", + 10: "ott", + 11: "nov", + 12: "dic", + }, + "narrow": { + 1: "G", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "G", + 7: "L", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "gennaio", + 2: "febbraio", + 3: "marzo", + 4: "aprile", + 5: "maggio", + 6: "giugno", + 7: "luglio", + 8: "agosto", + 9: "settembre", + 10: "ottobre", + 11: "novembre", + 12: "dicembre", + }, + }, + "units": { + "year": {"one": "{0} anno", "other": "{0} anni"}, + "month": {"one": "{0} mese", "other": "{0} mesi"}, + "week": {"one": "{0} settimana", "other": "{0} settimane"}, + "day": {"one": "{0} giorno", "other": "{0} giorni"}, + "hour": {"one": "{0} ora", "other": "{0} ore"}, + "minute": {"one": "{0} minuto", "other": "{0} minuti"}, + "second": {"one": "{0} secondo", "other": "{0} secondi"}, + "microsecond": {"one": "{0} microsecondo", "other": "{0} microsecondi"}, + }, + "relative": { + "year": { + "future": {"other": "tra {0} anni", "one": "tra {0} anno"}, + "past": {"other": "{0} anni fa", "one": "{0} anno fa"}, + }, + "month": { + "future": {"other": "tra {0} mesi", "one": "tra {0} mese"}, + "past": {"other": "{0} mesi fa", "one": "{0} mese fa"}, + }, + "week": { + "future": {"other": "tra {0} settimane", "one": "tra {0} settimana"}, + "past": {"other": "{0} settimane fa", "one": "{0} settimana fa"}, + }, + "day": { + "future": {"other": "tra {0} giorni", "one": "tra {0} giorno"}, + "past": {"other": "{0} giorni fa", "one": "{0} giorno fa"}, + }, + "hour": { + "future": {"other": "tra {0} ore", "one": "tra {0} ora"}, + "past": {"other": "{0} ore fa", "one": "{0} ora fa"}, + }, + "minute": { + "future": {"other": "tra {0} minuti", "one": "tra {0} minuto"}, + "past": {"other": "{0} minuti fa", "one": "{0} minuto fa"}, + }, + "second": { + "future": {"other": "tra {0} secondi", "one": "tra {0} secondo"}, + "past": {"other": "{0} secondi fa", "one": "{0} secondo fa"}, + }, + }, + "day_periods": { + "midnight": "mezzanotte", + "am": "AM", + "noon": "mezzogiorno", + "pm": "PM", + "morning1": "di mattina", + "afternoon1": "del pomeriggio", + "evening1": "di sera", + "night1": "di notte", + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/ko/__init__.py b/pendulum/locales/ko/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/ko/__init__.py diff --git a/pendulum/locales/ko/custom.py b/pendulum/locales/ko/custom.py new file mode 100644 index 0000000..0dd6a11 --- /dev/null +++ b/pendulum/locales/ko/custom.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +ko custom locale file. +""" + +translations = { + # Relative time + "after": "{0} 뒤", + "before": "{0} 앞", + # Date formats + "date_formats": { + "LTS": "A h시 m분 s초", + "LT": "A h시 m분", + "LLLL": "YYYY년 MMMM D일 dddd A h시 m분", + "LLL": "YYYY년 MMMM D일 A h시 m분", + "LL": "YYYY년 MMMM D일", + "L": "YYYY.MM.DD", + }, +} diff --git a/pendulum/locales/ko/locale.py b/pendulum/locales/ko/locale.py new file mode 100644 index 0000000..dfdb35a --- /dev/null +++ b/pendulum/locales/ko/locale.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +ko locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": {0: "일", 1: "월", 2: "화", 3: "수", 4: "목", 5: "금", 6: "토"}, + "narrow": {0: "일", 1: "월", 2: "화", 3: "수", 4: "목", 5: "금", 6: "토"}, + "short": {0: "일", 1: "월", 2: "화", 3: "수", 4: "목", 5: "금", 6: "토"}, + "wide": { + 0: "일요일", + 1: "월요일", + 2: "화요일", + 3: "수요일", + 4: "목요일", + 5: "금요일", + 6: "토요일", + }, + }, + "months": { + "abbreviated": { + 1: "1월", + 2: "2월", + 3: "3월", + 4: "4월", + 5: "5월", + 6: "6월", + 7: "7월", + 8: "8월", + 9: "9월", + 10: "10월", + 11: "11월", + 12: "12월", + }, + "narrow": { + 1: "1월", + 2: "2월", + 3: "3월", + 4: "4월", + 5: "5월", + 6: "6월", + 7: "7월", + 8: "8월", + 9: "9월", + 10: "10월", + 11: "11월", + 12: "12월", + }, + "wide": { + 1: "1월", + 2: "2월", + 3: "3월", + 4: "4월", + 5: "5월", + 6: "6월", + 7: "7월", + 8: "8월", + 9: "9월", + 10: "10월", + 11: "11월", + 12: "12월", + }, + }, + "units": { + "year": {"other": "{0}년"}, + "month": {"other": "{0}개월"}, + "week": {"other": "{0}주"}, + "day": {"other": "{0}일"}, + "hour": {"other": "{0}시간"}, + "minute": {"other": "{0}분"}, + "second": {"other": "{0}초"}, + "microsecond": {"other": "{0}마이크로초"}, + }, + "relative": { + "year": {"future": {"other": "{0}년 후"}, "past": {"other": "{0}년 전"}}, + "month": {"future": {"other": "{0}개월 후"}, "past": {"other": "{0}개월 전"}}, + "week": {"future": {"other": "{0}주 후"}, "past": {"other": "{0}주 전"}}, + "day": {"future": {"other": "{0}일 후"}, "past": {"other": "{0}일 전"}}, + "hour": {"future": {"other": "{0}시간 후"}, "past": {"other": "{0}시간 전"}}, + "minute": {"future": {"other": "{0}분 후"}, "past": {"other": "{0}분 전"}}, + "second": {"future": {"other": "{0}초 후"}, "past": {"other": "{0}초 전"}}, + }, + "day_periods": { + "midnight": "자정", + "am": "오전", + "noon": "정오", + "pm": "오후", + "morning1": "새벽", + "morning2": "오전", + "afternoon1": "오후", + "evening1": "저녁", + "night1": "밤", + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/locale.py b/pendulum/locales/locale.py new file mode 100644 index 0000000..de4cd82 --- /dev/null +++ b/pendulum/locales/locale.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import os +import re + +from importlib import import_module +from typing import Any +from typing import Optional +from typing import Union + +from pendulum.utils._compat import basestring +from pendulum.utils._compat import decode + + +class Locale: + """ + Represent a specific locale. + """ + + _cache = {} + + def __init__(self, locale, data): # type: (str, Any) -> None + self._locale = locale + self._data = data + self._key_cache = {} + + @classmethod + def load(cls, locale): # type: (Union[str, Locale]) -> Locale + if isinstance(locale, Locale): + return locale + + locale = cls.normalize_locale(locale) + if locale in cls._cache: + return cls._cache[locale] + + # Checking locale existence + actual_locale = locale + locale_path = os.path.join(os.path.dirname(__file__), actual_locale) + while not os.path.exists(locale_path): + if actual_locale == locale: + raise ValueError("Locale [{}] does not exist.".format(locale)) + + actual_locale = actual_locale.split("_")[0] + + m = import_module("pendulum.locales.{}.locale".format(actual_locale)) + + cls._cache[locale] = cls(locale, m.locale) + + return cls._cache[locale] + + @classmethod + def normalize_locale(cls, locale): # type: (str) -> str + m = re.match("([a-z]{2})[-_]([a-z]{2})", locale, re.I) + if m: + return "{}_{}".format(m.group(1).lower(), m.group(2).lower()) + else: + return locale.lower() + + def get(self, key, default=None): # type: (str, Optional[Any]) -> Any + if key in self._key_cache: + return self._key_cache[key] + + parts = key.split(".") + try: + result = self._data[parts[0]] + for part in parts[1:]: + result = result[part] + except KeyError: + result = default + + if isinstance(result, basestring): + result = decode(result) + + self._key_cache[key] = result + + return self._key_cache[key] + + def translation(self, key): # type: (str) -> Any + return self.get("translations.{}".format(key)) + + def plural(self, number): # type: (int) -> str + return decode(self._data["plural"](number)) + + def ordinal(self, number): # type: (int) -> str + return decode(self._data["ordinal"](number)) + + def ordinalize(self, number): # type: (int) -> str + ordinal = self.get("custom.ordinal.{}".format(self.ordinal(number))) + + if not ordinal: + return decode("{}".format(number)) + + return decode("{}{}".format(number, ordinal)) + + def match_translation(self, key, value): + translations = self.translation(key) + if value not in translations.values(): + return None + + return {v: k for k, v in translations.items()}[value] + + def __repr__(self): + return "{}('{}')".format(self.__class__.__name__, self._locale) diff --git a/pendulum/locales/lt/__init__.py b/pendulum/locales/lt/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/lt/__init__.py diff --git a/pendulum/locales/lt/custom.py b/pendulum/locales/lt/custom.py new file mode 100644 index 0000000..11c9980 --- /dev/null +++ b/pendulum/locales/lt/custom.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +lt custom locale file. +""" + +translations = { + # Relative time + "units_relative": { + "year": { + "future": { + "other": "{0} metų", + "one": "{0} metų", + "few": "{0} metų", + "many": "{0} metų", + }, + "past": { + "other": "{0} metų", + "one": "{0} metus", + "few": "{0} metus", + "many": "{0} metų", + }, + }, + "month": { + "future": { + "other": "{0} mėnesių", + "one": "{0} mėnesio", + "few": "{0} mėnesių", + "many": "{0} mėnesio", + }, + "past": { + "other": "{0} mėnesių", + "one": "{0} mėnesį", + "few": "{0} mėnesius", + "many": "{0} mėnesio", + }, + }, + "week": { + "future": { + "other": "{0} savaičių", + "one": "{0} savaitės", + "few": "{0} savaičių", + "many": "{0} savaitės", + }, + "past": { + "other": "{0} savaičių", + "one": "{0} savaitę", + "few": "{0} savaites", + "many": "{0} savaitės", + }, + }, + "day": { + "future": { + "other": "{0} dienų", + "one": "{0} dienos", + "few": "{0} dienų", + "many": "{0} dienos", + }, + "past": { + "other": "{0} dienų", + "one": "{0} dieną", + "few": "{0} dienas", + "many": "{0} dienos", + }, + }, + "hour": { + "future": { + "other": "{0} valandų", + "one": "{0} valandos", + "few": "{0} valandų", + "many": "{0} valandos", + }, + "past": { + "other": "{0} valandų", + "one": "{0} valandą", + "few": "{0} valandas", + "many": "{0} valandos", + }, + }, + "minute": { + "future": { + "other": "{0} minučių", + "one": "{0} minutės", + "few": "{0} minučių", + "many": "{0} minutės", + }, + "past": { + "other": "{0} minučių", + "one": "{0} minutę", + "few": "{0} minutes", + "many": "{0} minutės", + }, + }, + "second": { + "future": { + "other": "{0} sekundžių", + "one": "{0} sekundės", + "few": "{0} sekundžių", + "many": "{0} sekundės", + }, + "past": { + "other": "{0} sekundžių", + "one": "{0} sekundę", + "few": "{0} sekundes", + "many": "{0} sekundės", + }, + }, + }, + "after": "po {0}", + "before": "{0} nuo dabar", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "YYYY [m.] MMMM D [d.], dddd, HH:mm [val.]", + "LLL": "YYYY [m.] MMMM D [d.], HH:mm [val.]", + "LL": "YYYY [m.] MMMM D [d.]", + "L": "YYYY-MM-DD", + }, +} diff --git a/pendulum/locales/lt/locale.py b/pendulum/locales/lt/locale.py new file mode 100644 index 0000000..12f1335 --- /dev/null +++ b/pendulum/locales/lt/locale.py @@ -0,0 +1,258 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +lt locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "few" + if ( + ((n % 10) == (n % 10) and (((n % 10) >= 2 and (n % 10) <= 9))) + and (not ((n % 100) == (n % 100) and (((n % 100) >= 11 and (n % 100) <= 19)))) + ) + else "many" + if (not (0 == 0 and ((0 == 0)))) + else "one" + if ( + ((n % 10) == (n % 10) and (((n % 10) == 1))) + and (not ((n % 100) == (n % 100) and (((n % 100) >= 11 and (n % 100) <= 19)))) + ) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "sk", + 1: "pr", + 2: "an", + 3: "tr", + 4: "kt", + 5: "pn", + 6: "št", + }, + "narrow": {0: "S", 1: "P", 2: "A", 3: "T", 4: "K", 5: "P", 6: "Š"}, + "short": {0: "Sk", 1: "Pr", 2: "An", 3: "Tr", 4: "Kt", 5: "Pn", 6: "Št"}, + "wide": { + 0: "sekmadienis", + 1: "pirmadienis", + 2: "antradienis", + 3: "trečiadienis", + 4: "ketvirtadienis", + 5: "penktadienis", + 6: "šeštadienis", + }, + }, + "months": { + "abbreviated": { + 1: "saus.", + 2: "vas.", + 3: "kov.", + 4: "bal.", + 5: "geg.", + 6: "birž.", + 7: "liep.", + 8: "rugp.", + 9: "rugs.", + 10: "spal.", + 11: "lapkr.", + 12: "gruod.", + }, + "narrow": { + 1: "S", + 2: "V", + 3: "K", + 4: "B", + 5: "G", + 6: "B", + 7: "L", + 8: "R", + 9: "R", + 10: "S", + 11: "L", + 12: "G", + }, + "wide": { + 1: "sausio", + 2: "vasario", + 3: "kovo", + 4: "balandžio", + 5: "gegužės", + 6: "birželio", + 7: "liepos", + 8: "rugpjūčio", + 9: "rugsėjo", + 10: "spalio", + 11: "lapkričio", + 12: "gruodžio", + }, + }, + "units": { + "year": { + "one": "{0} metai", + "few": "{0} metai", + "many": "{0} metų", + "other": "{0} metų", + }, + "month": { + "one": "{0} mėnuo", + "few": "{0} mėnesiai", + "many": "{0} mėnesio", + "other": "{0} mėnesių", + }, + "week": { + "one": "{0} savaitė", + "few": "{0} savaitės", + "many": "{0} savaitės", + "other": "{0} savaičių", + }, + "day": { + "one": "{0} diena", + "few": "{0} dienos", + "many": "{0} dienos", + "other": "{0} dienų", + }, + "hour": { + "one": "{0} valanda", + "few": "{0} valandos", + "many": "{0} valandos", + "other": "{0} valandų", + }, + "minute": { + "one": "{0} minutė", + "few": "{0} minutės", + "many": "{0} minutės", + "other": "{0} minučių", + }, + "second": { + "one": "{0} sekundė", + "few": "{0} sekundės", + "many": "{0} sekundės", + "other": "{0} sekundžių", + }, + "microsecond": { + "one": "{0} mikrosekundė", + "few": "{0} mikrosekundės", + "many": "{0} mikrosekundės", + "other": "{0} mikrosekundžių", + }, + }, + "relative": { + "year": { + "future": { + "other": "po {0} metų", + "one": "po {0} metų", + "few": "po {0} metų", + "many": "po {0} metų", + }, + "past": { + "other": "prieš {0} metų", + "one": "prieš {0} metus", + "few": "prieš {0} metus", + "many": "prieš {0} metų", + }, + }, + "month": { + "future": { + "other": "po {0} mėnesių", + "one": "po {0} mėnesio", + "few": "po {0} mėnesių", + "many": "po {0} mėnesio", + }, + "past": { + "other": "prieš {0} mėnesių", + "one": "prieš {0} mėnesį", + "few": "prieš {0} mėnesius", + "many": "prieš {0} mėnesio", + }, + }, + "week": { + "future": { + "other": "po {0} savaičių", + "one": "po {0} savaitės", + "few": "po {0} savaičių", + "many": "po {0} savaitės", + }, + "past": { + "other": "prieš {0} savaičių", + "one": "prieš {0} savaitę", + "few": "prieš {0} savaites", + "many": "prieš {0} savaitės", + }, + }, + "day": { + "future": { + "other": "po {0} dienų", + "one": "po {0} dienos", + "few": "po {0} dienų", + "many": "po {0} dienos", + }, + "past": { + "other": "prieš {0} dienų", + "one": "prieš {0} dieną", + "few": "prieš {0} dienas", + "many": "prieš {0} dienos", + }, + }, + "hour": { + "future": { + "other": "po {0} valandų", + "one": "po {0} valandos", + "few": "po {0} valandų", + "many": "po {0} valandos", + }, + "past": { + "other": "prieš {0} valandų", + "one": "prieš {0} valandą", + "few": "prieš {0} valandas", + "many": "prieš {0} valandos", + }, + }, + "minute": { + "future": { + "other": "po {0} minučių", + "one": "po {0} minutės", + "few": "po {0} minučių", + "many": "po {0} minutės", + }, + "past": { + "other": "prieš {0} minučių", + "one": "prieš {0} minutę", + "few": "prieš {0} minutes", + "many": "prieš {0} minutės", + }, + }, + "second": { + "future": { + "other": "po {0} sekundžių", + "one": "po {0} sekundės", + "few": "po {0} sekundžių", + "many": "po {0} sekundės", + }, + "past": { + "other": "prieš {0} sekundžių", + "one": "prieš {0} sekundę", + "few": "prieš {0} sekundes", + "many": "prieš {0} sekundės", + }, + }, + }, + "day_periods": { + "midnight": "vidurnaktis", + "am": "priešpiet", + "noon": "perpiet", + "pm": "popiet", + "morning1": "rytas", + "afternoon1": "popietė", + "evening1": "vakaras", + "night1": "naktis", + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/nb/__init__.py b/pendulum/locales/nb/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/nb/__init__.py diff --git a/pendulum/locales/nb/custom.py b/pendulum/locales/nb/custom.py new file mode 100644 index 0000000..216dd04 --- /dev/null +++ b/pendulum/locales/nb/custom.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +nn custom locale file. +""" + +translations = { + # Relative time + "after": "{0} etter", + "before": "{0} før", + # Ordinals + "ordinal": {"one": ".", "two": ".", "few": ".", "other": "."}, + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd Do MMMM YYYY HH:mm", + "LLL": "Do MMMM YYYY HH:mm", + "LL": "Do MMMM YYYY", + "L": "DD.MM.YYYY", + }, +} diff --git a/pendulum/locales/nb/locale.py b/pendulum/locales/nb/locale.py new file mode 100644 index 0000000..0ad08d1 --- /dev/null +++ b/pendulum/locales/nb/locale.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +nb locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" if (n == n and ((n == 1))) else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "søn.", + 1: "man.", + 2: "tir.", + 3: "ons.", + 4: "tor.", + 5: "fre.", + 6: "lør.", + }, + "narrow": {0: "S", 1: "M", 2: "T", 3: "O", 4: "T", 5: "F", 6: "L"}, + "short": { + 0: "sø.", + 1: "ma.", + 2: "ti.", + 3: "on.", + 4: "to.", + 5: "fr.", + 6: "lø.", + }, + "wide": { + 0: "søndag", + 1: "mandag", + 2: "tirsdag", + 3: "onsdag", + 4: "torsdag", + 5: "fredag", + 6: "lørdag", + }, + }, + "months": { + "abbreviated": { + 1: "jan.", + 2: "feb.", + 3: "mar.", + 4: "apr.", + 5: "mai", + 6: "jun.", + 7: "jul.", + 8: "aug.", + 9: "sep.", + 10: "okt.", + 11: "nov.", + 12: "des.", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "januar", + 2: "februar", + 3: "mars", + 4: "april", + 5: "mai", + 6: "juni", + 7: "juli", + 8: "august", + 9: "september", + 10: "oktober", + 11: "november", + 12: "desember", + }, + }, + "units": { + "year": {"one": "{0} år", "other": "{0} år"}, + "month": {"one": "{0} måned", "other": "{0} måneder"}, + "week": {"one": "{0} uke", "other": "{0} uker"}, + "day": {"one": "{0} dag", "other": "{0} dager"}, + "hour": {"one": "{0} time", "other": "{0} timer"}, + "minute": {"one": "{0} minutt", "other": "{0} minutter"}, + "second": {"one": "{0} sekund", "other": "{0} sekunder"}, + "microsecond": {"one": "{0} mikrosekund", "other": "{0} mikrosekunder"}, + }, + "relative": { + "year": { + "future": {"other": "om {0} år", "one": "om {0} år"}, + "past": {"other": "for {0} år siden", "one": "for {0} år siden"}, + }, + "month": { + "future": {"other": "om {0} måneder", "one": "om {0} måned"}, + "past": { + "other": "for {0} måneder siden", + "one": "for {0} måned siden", + }, + }, + "week": { + "future": {"other": "om {0} uker", "one": "om {0} uke"}, + "past": {"other": "for {0} uker siden", "one": "for {0} uke siden"}, + }, + "day": { + "future": {"other": "om {0} dager", "one": "om {0} dag"}, + "past": {"other": "for {0} dager siden", "one": "for {0} dag siden"}, + }, + "hour": { + "future": {"other": "om {0} timer", "one": "om {0} time"}, + "past": {"other": "for {0} timer siden", "one": "for {0} time siden"}, + }, + "minute": { + "future": {"other": "om {0} minutter", "one": "om {0} minutt"}, + "past": { + "other": "for {0} minutter siden", + "one": "for {0} minutt siden", + }, + }, + "second": { + "future": {"other": "om {0} sekunder", "one": "om {0} sekund"}, + "past": { + "other": "for {0} sekunder siden", + "one": "for {0} sekund siden", + }, + }, + }, + "day_periods": { + "midnight": "midnatt", + "am": "a.m.", + "pm": "p.m.", + "morning1": "morgenen", + "morning2": "formiddagen", + "afternoon1": "ettermiddagen", + "evening1": "kvelden", + "night1": "natten", + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/nl/__init__.py b/pendulum/locales/nl/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/nl/__init__.py diff --git a/pendulum/locales/nl/custom.py b/pendulum/locales/nl/custom.py new file mode 100644 index 0000000..2b8790e --- /dev/null +++ b/pendulum/locales/nl/custom.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +nl custom locale file. +""" + +translations = { + "units": {"few_second": "enkele seconden"}, + # Relative time + "ago": "{} geleden", + "from_now": "over {}", + "after": "{0} later", + "before": "{0} eerder", + # Ordinals + "ordinal": {"other": "e"}, + # Date formats + "date_formats": { + "L": "DD-MM-YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY HH:mm", + "LLLL": "dddd D MMMM YYYY HH:mm", + "LT": "HH:mm", + "LTS": "HH:mm:ss", + }, +} diff --git a/pendulum/locales/nl/locale.py b/pendulum/locales/nl/locale.py new file mode 100644 index 0000000..1e4d67e --- /dev/null +++ b/pendulum/locales/nl/locale.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +nl locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and ((n == 1))) and (0 == 0 and ((0 == 0)))) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "zo", + 1: "ma", + 2: "di", + 3: "wo", + 4: "do", + 5: "vr", + 6: "za", + }, + "narrow": {0: "Z", 1: "M", 2: "D", 3: "W", 4: "D", 5: "V", 6: "Z"}, + "short": {0: "zo", 1: "ma", 2: "di", 3: "wo", 4: "do", 5: "vr", 6: "za"}, + "wide": { + 0: "zondag", + 1: "maandag", + 2: "dinsdag", + 3: "woensdag", + 4: "donderdag", + 5: "vrijdag", + 6: "zaterdag", + }, + }, + "months": { + "abbreviated": { + 1: "jan.", + 2: "feb.", + 3: "mrt.", + 4: "apr.", + 5: "mei", + 6: "jun.", + 7: "jul.", + 8: "aug.", + 9: "sep.", + 10: "okt.", + 11: "nov.", + 12: "dec.", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "januari", + 2: "februari", + 3: "maart", + 4: "april", + 5: "mei", + 6: "juni", + 7: "juli", + 8: "augustus", + 9: "september", + 10: "oktober", + 11: "november", + 12: "december", + }, + }, + "units": { + "year": {"one": "{0} jaar", "other": "{0} jaar"}, + "month": {"one": "{0} maand", "other": "{0} maanden"}, + "week": {"one": "{0} week", "other": "{0} weken"}, + "day": {"one": "{0} dag", "other": "{0} dagen"}, + "hour": {"one": "{0} uur", "other": "{0} uur"}, + "minute": {"one": "{0} minuut", "other": "{0} minuten"}, + "second": {"one": "{0} seconde", "other": "{0} seconden"}, + "microsecond": {"one": "{0} microseconde", "other": "{0} microseconden"}, + }, + "relative": { + "year": { + "future": {"other": "over {0} jaar", "one": "over {0} jaar"}, + "past": {"other": "{0} jaar geleden", "one": "{0} jaar geleden"}, + }, + "month": { + "future": {"other": "over {0} maanden", "one": "over {0} maand"}, + "past": {"other": "{0} maanden geleden", "one": "{0} maand geleden"}, + }, + "week": { + "future": {"other": "over {0} weken", "one": "over {0} week"}, + "past": {"other": "{0} weken geleden", "one": "{0} week geleden"}, + }, + "day": { + "future": {"other": "over {0} dagen", "one": "over {0} dag"}, + "past": {"other": "{0} dagen geleden", "one": "{0} dag geleden"}, + }, + "hour": { + "future": {"other": "over {0} uur", "one": "over {0} uur"}, + "past": {"other": "{0} uur geleden", "one": "{0} uur geleden"}, + }, + "minute": { + "future": {"other": "over {0} minuten", "one": "over {0} minuut"}, + "past": {"other": "{0} minuten geleden", "one": "{0} minuut geleden"}, + }, + "second": { + "future": {"other": "over {0} seconden", "one": "over {0} seconde"}, + "past": {"other": "{0} seconden geleden", "one": "{0} seconde geleden"}, + }, + }, + "day_periods": { + "midnight": "middernacht", + "am": "a.m.", + "pm": "p.m.", + "morning1": "‘s ochtends", + "afternoon1": "‘s middags", + "evening1": "‘s avonds", + "night1": "‘s nachts", + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/nn/__init__.py b/pendulum/locales/nn/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/nn/__init__.py diff --git a/pendulum/locales/nn/custom.py b/pendulum/locales/nn/custom.py new file mode 100644 index 0000000..216dd04 --- /dev/null +++ b/pendulum/locales/nn/custom.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +nn custom locale file. +""" + +translations = { + # Relative time + "after": "{0} etter", + "before": "{0} før", + # Ordinals + "ordinal": {"one": ".", "two": ".", "few": ".", "other": "."}, + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd Do MMMM YYYY HH:mm", + "LLL": "Do MMMM YYYY HH:mm", + "LL": "Do MMMM YYYY", + "L": "DD.MM.YYYY", + }, +} diff --git a/pendulum/locales/nn/locale.py b/pendulum/locales/nn/locale.py new file mode 100644 index 0000000..d7ad790 --- /dev/null +++ b/pendulum/locales/nn/locale.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +nn locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" if (n == n and ((n == 1))) else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "søn.", + 1: "mån.", + 2: "tys.", + 3: "ons.", + 4: "tor.", + 5: "fre.", + 6: "lau.", + }, + "narrow": {0: "S", 1: "M", 2: "T", 3: "O", 4: "T", 5: "F", 6: "L"}, + "short": { + 0: "sø.", + 1: "må.", + 2: "ty.", + 3: "on.", + 4: "to.", + 5: "fr.", + 6: "la.", + }, + "wide": { + 0: "søndag", + 1: "måndag", + 2: "tysdag", + 3: "onsdag", + 4: "torsdag", + 5: "fredag", + 6: "laurdag", + }, + }, + "months": { + "abbreviated": { + 1: "jan.", + 2: "feb.", + 3: "mars", + 4: "apr.", + 5: "mai", + 6: "juni", + 7: "juli", + 8: "aug.", + 9: "sep.", + 10: "okt.", + 11: "nov.", + 12: "des.", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "januar", + 2: "februar", + 3: "mars", + 4: "april", + 5: "mai", + 6: "juni", + 7: "juli", + 8: "august", + 9: "september", + 10: "oktober", + 11: "november", + 12: "desember", + }, + }, + "units": { + "year": {"one": "{0} år", "other": "{0} år"}, + "month": {"one": "{0} månad", "other": "{0} månadar"}, + "week": {"one": "{0} veke", "other": "{0} veker"}, + "day": {"one": "{0} dag", "other": "{0} dagar"}, + "hour": {"one": "{0} time", "other": "{0} timar"}, + "minute": {"one": "{0} minutt", "other": "{0} minutt"}, + "second": {"one": "{0} sekund", "other": "{0} sekund"}, + "microsecond": {"one": "{0} mikrosekund", "other": "{0} mikrosekund"}, + }, + "relative": { + "year": { + "future": {"other": "om {0} år", "one": "om {0} år"}, + "past": {"other": "for {0} år sidan", "one": "for {0} år sidan"}, + }, + "month": { + "future": {"other": "om {0} månadar", "one": "om {0} månad"}, + "past": { + "other": "for {0} månadar sidan", + "one": "for {0} månad sidan", + }, + }, + "week": { + "future": {"other": "om {0} veker", "one": "om {0} veke"}, + "past": {"other": "for {0} veker sidan", "one": "for {0} veke sidan"}, + }, + "day": { + "future": {"other": "om {0} dagar", "one": "om {0} dag"}, + "past": {"other": "for {0} dagar sidan", "one": "for {0} dag sidan"}, + }, + "hour": { + "future": {"other": "om {0} timar", "one": "om {0} time"}, + "past": {"other": "for {0} timar sidan", "one": "for {0} time sidan"}, + }, + "minute": { + "future": {"other": "om {0} minutt", "one": "om {0} minutt"}, + "past": { + "other": "for {0} minutt sidan", + "one": "for {0} minutt sidan", + }, + }, + "second": { + "future": {"other": "om {0} sekund", "one": "om {0} sekund"}, + "past": { + "other": "for {0} sekund sidan", + "one": "for {0} sekund sidan", + }, + }, + }, + "day_periods": {"am": "formiddag", "pm": "ettermiddag"}, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/pl/__init__.py b/pendulum/locales/pl/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/pl/__init__.py diff --git a/pendulum/locales/pl/custom.py b/pendulum/locales/pl/custom.py new file mode 100644 index 0000000..d93465e --- /dev/null +++ b/pendulum/locales/pl/custom.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +pl custom locale file. +""" + +translations = { + "units": {"few_second": "kilka sekund"}, + # Relative time + "ago": "{} temu", + "from_now": "za {}", + "after": "{0} po", + "before": "{0} przed", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "L": "DD.MM.YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY HH:mm", + "LLLL": "dddd, D MMMM YYYY HH:mm", + }, +} diff --git a/pendulum/locales/pl/locale.py b/pendulum/locales/pl/locale.py new file mode 100644 index 0000000..7f83ee5 --- /dev/null +++ b/pendulum/locales/pl/locale.py @@ -0,0 +1,282 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +pl locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "few" + if ( + ( + (0 == 0 and ((0 == 0))) + and ((n % 10) == (n % 10) and (((n % 10) >= 2 and (n % 10) <= 4))) + ) + and (not ((n % 100) == (n % 100) and (((n % 100) >= 12 and (n % 100) <= 14)))) + ) + else "many" + if ( + ( + ( + ((0 == 0 and ((0 == 0))) and (not (n == n and ((n == 1))))) + and ((n % 10) == (n % 10) and (((n % 10) >= 0 and (n % 10) <= 1))) + ) + or ( + (0 == 0 and ((0 == 0))) + and ((n % 10) == (n % 10) and (((n % 10) >= 5 and (n % 10) <= 9))) + ) + ) + or ( + (0 == 0 and ((0 == 0))) + and ((n % 100) == (n % 100) and (((n % 100) >= 12 and (n % 100) <= 14))) + ) + ) + else "one" + if ((n == n and ((n == 1))) and (0 == 0 and ((0 == 0)))) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "niedz.", + 1: "pon.", + 2: "wt.", + 3: "śr.", + 4: "czw.", + 5: "pt.", + 6: "sob.", + }, + "narrow": {0: "n", 1: "p", 2: "w", 3: "ś", 4: "c", 5: "p", 6: "s"}, + "short": { + 0: "nie", + 1: "pon", + 2: "wto", + 3: "śro", + 4: "czw", + 5: "pią", + 6: "sob", + }, + "wide": { + 0: "niedziela", + 1: "poniedziałek", + 2: "wtorek", + 3: "środa", + 4: "czwartek", + 5: "piątek", + 6: "sobota", + }, + }, + "months": { + "abbreviated": { + 1: "sty", + 2: "lut", + 3: "mar", + 4: "kwi", + 5: "maj", + 6: "cze", + 7: "lip", + 8: "sie", + 9: "wrz", + 10: "paź", + 11: "lis", + 12: "gru", + }, + "narrow": { + 1: "s", + 2: "l", + 3: "m", + 4: "k", + 5: "m", + 6: "c", + 7: "l", + 8: "s", + 9: "w", + 10: "p", + 11: "l", + 12: "g", + }, + "wide": { + 1: "stycznia", + 2: "lutego", + 3: "marca", + 4: "kwietnia", + 5: "maja", + 6: "czerwca", + 7: "lipca", + 8: "sierpnia", + 9: "września", + 10: "października", + 11: "listopada", + 12: "grudnia", + }, + }, + "units": { + "year": { + "one": "{0} rok", + "few": "{0} lata", + "many": "{0} lat", + "other": "{0} roku", + }, + "month": { + "one": "{0} miesiąc", + "few": "{0} miesiące", + "many": "{0} miesięcy", + "other": "{0} miesiąca", + }, + "week": { + "one": "{0} tydzień", + "few": "{0} tygodnie", + "many": "{0} tygodni", + "other": "{0} tygodnia", + }, + "day": { + "one": "{0} dzień", + "few": "{0} dni", + "many": "{0} dni", + "other": "{0} dnia", + }, + "hour": { + "one": "{0} godzina", + "few": "{0} godziny", + "many": "{0} godzin", + "other": "{0} godziny", + }, + "minute": { + "one": "{0} minuta", + "few": "{0} minuty", + "many": "{0} minut", + "other": "{0} minuty", + }, + "second": { + "one": "{0} sekunda", + "few": "{0} sekundy", + "many": "{0} sekund", + "other": "{0} sekundy", + }, + "microsecond": { + "one": "{0} mikrosekunda", + "few": "{0} mikrosekundy", + "many": "{0} mikrosekund", + "other": "{0} mikrosekundy", + }, + }, + "relative": { + "year": { + "future": { + "other": "za {0} roku", + "one": "za {0} rok", + "few": "za {0} lata", + "many": "za {0} lat", + }, + "past": { + "other": "{0} roku temu", + "one": "{0} rok temu", + "few": "{0} lata temu", + "many": "{0} lat temu", + }, + }, + "month": { + "future": { + "other": "za {0} miesiąca", + "one": "za {0} miesiąc", + "few": "za {0} miesiące", + "many": "za {0} miesięcy", + }, + "past": { + "other": "{0} miesiąca temu", + "one": "{0} miesiąc temu", + "few": "{0} miesiące temu", + "many": "{0} miesięcy temu", + }, + }, + "week": { + "future": { + "other": "za {0} tygodnia", + "one": "za {0} tydzień", + "few": "za {0} tygodnie", + "many": "za {0} tygodni", + }, + "past": { + "other": "{0} tygodnia temu", + "one": "{0} tydzień temu", + "few": "{0} tygodnie temu", + "many": "{0} tygodni temu", + }, + }, + "day": { + "future": { + "other": "za {0} dnia", + "one": "za {0} dzień", + "few": "za {0} dni", + "many": "za {0} dni", + }, + "past": { + "other": "{0} dnia temu", + "one": "{0} dzień temu", + "few": "{0} dni temu", + "many": "{0} dni temu", + }, + }, + "hour": { + "future": { + "other": "za {0} godziny", + "one": "za {0} godzinę", + "few": "za {0} godziny", + "many": "za {0} godzin", + }, + "past": { + "other": "{0} godziny temu", + "one": "{0} godzinę temu", + "few": "{0} godziny temu", + "many": "{0} godzin temu", + }, + }, + "minute": { + "future": { + "other": "za {0} minuty", + "one": "za {0} minutę", + "few": "za {0} minuty", + "many": "za {0} minut", + }, + "past": { + "other": "{0} minuty temu", + "one": "{0} minutę temu", + "few": "{0} minuty temu", + "many": "{0} minut temu", + }, + }, + "second": { + "future": { + "other": "za {0} sekundy", + "one": "za {0} sekundę", + "few": "za {0} sekundy", + "many": "za {0} sekund", + }, + "past": { + "other": "{0} sekundy temu", + "one": "{0} sekundę temu", + "few": "{0} sekundy temu", + "many": "{0} sekund temu", + }, + }, + }, + "day_periods": { + "midnight": "o północy", + "am": "AM", + "noon": "w południe", + "pm": "PM", + "morning1": "rano", + "morning2": "przed południem", + "afternoon1": "po południu", + "evening1": "wieczorem", + "night1": "w nocy", + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/pt_br/__init__.py b/pendulum/locales/pt_br/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/pt_br/__init__.py diff --git a/pendulum/locales/pt_br/custom.py b/pendulum/locales/pt_br/custom.py new file mode 100644 index 0000000..065a140 --- /dev/null +++ b/pendulum/locales/pt_br/custom.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +pt-br custom locale file. +""" + +translations = { + # Relative time + "after": "após {0}", + "before": "{0} atrás", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd, D [de] MMMM [de] YYYY [às] HH:mm", + "LLL": "D [de] MMMM [de] YYYY [às] HH:mm", + "LL": "D [de] MMMM [de] YYYY", + "L": "DD/MM/YYYY", + }, +} diff --git a/pendulum/locales/pt_br/locale.py b/pendulum/locales/pt_br/locale.py new file mode 100644 index 0000000..307f34f --- /dev/null +++ b/pendulum/locales/pt_br/locale.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +pt_br locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and ((n >= 0 and n <= 2))) and (not (n == n and ((n == 2))))) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "dom", + 1: "seg", + 2: "ter", + 3: "qua", + 4: "qui", + 5: "sex", + 6: "sáb", + }, + "narrow": {0: "D", 1: "S", 2: "T", 3: "Q", 4: "Q", 5: "S", 6: "S"}, + "short": { + 0: "dom", + 1: "seg", + 2: "ter", + 3: "qua", + 4: "qui", + 5: "sex", + 6: "sáb", + }, + "wide": { + 0: "domingo", + 1: "segunda-feira", + 2: "terça-feira", + 3: "quarta-feira", + 4: "quinta-feira", + 5: "sexta-feira", + 6: "sábado", + }, + }, + "months": { + "abbreviated": { + 1: "jan", + 2: "fev", + 3: "mar", + 4: "abr", + 5: "mai", + 6: "jun", + 7: "jul", + 8: "ago", + 9: "set", + 10: "out", + 11: "nov", + 12: "dez", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "janeiro", + 2: "fevereiro", + 3: "março", + 4: "abril", + 5: "maio", + 6: "junho", + 7: "julho", + 8: "agosto", + 9: "setembro", + 10: "outubro", + 11: "novembro", + 12: "dezembro", + }, + }, + "units": { + "year": {"one": "{0} ano", "other": "{0} anos"}, + "month": {"one": "{0} mês", "other": "{0} meses"}, + "week": {"one": "{0} semana", "other": "{0} semanas"}, + "day": {"one": "{0} dia", "other": "{0} dias"}, + "hour": {"one": "{0} hora", "other": "{0} horas"}, + "minute": {"one": "{0} minuto", "other": "{0} minutos"}, + "second": {"one": "{0} segundo", "other": "{0} segundos"}, + "microsecond": {"one": "{0} microssegundo", "other": "{0} microssegundos"}, + }, + "relative": { + "year": { + "future": {"other": "em {0} anos", "one": "em {0} ano"}, + "past": {"other": "há {0} anos", "one": "há {0} ano"}, + }, + "month": { + "future": {"other": "em {0} meses", "one": "em {0} mês"}, + "past": {"other": "há {0} meses", "one": "há {0} mês"}, + }, + "week": { + "future": {"other": "em {0} semanas", "one": "em {0} semana"}, + "past": {"other": "há {0} semanas", "one": "há {0} semana"}, + }, + "day": { + "future": {"other": "em {0} dias", "one": "em {0} dia"}, + "past": {"other": "há {0} dias", "one": "há {0} dia"}, + }, + "hour": { + "future": {"other": "em {0} horas", "one": "em {0} hora"}, + "past": {"other": "há {0} horas", "one": "há {0} hora"}, + }, + "minute": { + "future": {"other": "em {0} minutos", "one": "em {0} minuto"}, + "past": {"other": "há {0} minutos", "one": "há {0} minuto"}, + }, + "second": { + "future": {"other": "em {0} segundos", "one": "em {0} segundo"}, + "past": {"other": "há {0} segundos", "one": "há {0} segundo"}, + }, + }, + "day_periods": { + "midnight": "meia-noite", + "am": "AM", + "noon": "meio-dia", + "pm": "PM", + "morning1": "da manhã", + "afternoon1": "da tarde", + "evening1": "da noite", + "night1": "da madrugada", + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/ru/__init__.py b/pendulum/locales/ru/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/ru/__init__.py diff --git a/pendulum/locales/ru/custom.py b/pendulum/locales/ru/custom.py new file mode 100644 index 0000000..38f86ef --- /dev/null +++ b/pendulum/locales/ru/custom.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +ru custom locale file. +""" + +translations = { + # Relative time + "ago": "{} назад", + "from_now": "через {}", + "after": "{0} после", + "before": "{0} до", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "L": "DD.MM.YYYY", + "LL": "D MMMM YYYY г.", + "LLL": "D MMMM YYYY г., HH:mm", + "LLLL": "dddd, D MMMM YYYY г., HH:mm", + }, +} diff --git a/pendulum/locales/ru/locale.py b/pendulum/locales/ru/locale.py new file mode 100644 index 0000000..a080035 --- /dev/null +++ b/pendulum/locales/ru/locale.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +ru locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "few" + if ( + ( + (0 == 0 and ((0 == 0))) + and ((n % 10) == (n % 10) and (((n % 10) >= 2 and (n % 10) <= 4))) + ) + and (not ((n % 100) == (n % 100) and (((n % 100) >= 12 and (n % 100) <= 14)))) + ) + else "many" + if ( + ( + ((0 == 0 and ((0 == 0))) and ((n % 10) == (n % 10) and (((n % 10) == 0)))) + or ( + (0 == 0 and ((0 == 0))) + and ((n % 10) == (n % 10) and (((n % 10) >= 5 and (n % 10) <= 9))) + ) + ) + or ( + (0 == 0 and ((0 == 0))) + and ((n % 100) == (n % 100) and (((n % 100) >= 11 and (n % 100) <= 14))) + ) + ) + else "one" + if ( + ((0 == 0 and ((0 == 0))) and ((n % 10) == (n % 10) and (((n % 10) == 1)))) + and (not ((n % 100) == (n % 100) and (((n % 100) == 11)))) + ) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "вс", + 1: "пн", + 2: "вт", + 3: "ср", + 4: "чт", + 5: "пт", + 6: "сб", + }, + "narrow": {0: "вс", 1: "пн", 2: "вт", 3: "ср", 4: "чт", 5: "пт", 6: "сб"}, + "short": {0: "вс", 1: "пн", 2: "вт", 3: "ср", 4: "чт", 5: "пт", 6: "сб"}, + "wide": { + 0: "воскресенье", + 1: "понедельник", + 2: "вторник", + 3: "среда", + 4: "четверг", + 5: "пятница", + 6: "суббота", + }, + }, + "months": { + "abbreviated": { + 1: "янв.", + 2: "февр.", + 3: "мар.", + 4: "апр.", + 5: "мая", + 6: "июн.", + 7: "июл.", + 8: "авг.", + 9: "сент.", + 10: "окт.", + 11: "нояб.", + 12: "дек.", + }, + "narrow": { + 1: "Я", + 2: "Ф", + 3: "М", + 4: "А", + 5: "М", + 6: "И", + 7: "И", + 8: "А", + 9: "С", + 10: "О", + 11: "Н", + 12: "Д", + }, + "wide": { + 1: "января", + 2: "февраля", + 3: "марта", + 4: "апреля", + 5: "мая", + 6: "июня", + 7: "июля", + 8: "августа", + 9: "сентября", + 10: "октября", + 11: "ноября", + 12: "декабря", + }, + }, + "units": { + "year": { + "one": "{0} год", + "few": "{0} года", + "many": "{0} лет", + "other": "{0} года", + }, + "month": { + "one": "{0} месяц", + "few": "{0} месяца", + "many": "{0} месяцев", + "other": "{0} месяца", + }, + "week": { + "one": "{0} неделя", + "few": "{0} недели", + "many": "{0} недель", + "other": "{0} недели", + }, + "day": { + "one": "{0} день", + "few": "{0} дня", + "many": "{0} дней", + "other": "{0} дня", + }, + "hour": { + "one": "{0} час", + "few": "{0} часа", + "many": "{0} часов", + "other": "{0} часа", + }, + "minute": { + "one": "{0} минута", + "few": "{0} минуты", + "many": "{0} минут", + "other": "{0} минуты", + }, + "second": { + "one": "{0} секунда", + "few": "{0} секунды", + "many": "{0} секунд", + "other": "{0} секунды", + }, + "microsecond": { + "one": "{0} микросекунда", + "few": "{0} микросекунды", + "many": "{0} микросекунд", + "other": "{0} микросекунды", + }, + }, + "relative": { + "year": { + "future": { + "other": "через {0} года", + "one": "через {0} год", + "few": "через {0} года", + "many": "через {0} лет", + }, + "past": { + "other": "{0} года назад", + "one": "{0} год назад", + "few": "{0} года назад", + "many": "{0} лет назад", + }, + }, + "month": { + "future": { + "other": "через {0} месяца", + "one": "через {0} месяц", + "few": "через {0} месяца", + "many": "через {0} месяцев", + }, + "past": { + "other": "{0} месяца назад", + "one": "{0} месяц назад", + "few": "{0} месяца назад", + "many": "{0} месяцев назад", + }, + }, + "week": { + "future": { + "other": "через {0} недели", + "one": "через {0} неделю", + "few": "через {0} недели", + "many": "через {0} недель", + }, + "past": { + "other": "{0} недели назад", + "one": "{0} неделю назад", + "few": "{0} недели назад", + "many": "{0} недель назад", + }, + }, + "day": { + "future": { + "other": "через {0} дня", + "one": "через {0} день", + "few": "через {0} дня", + "many": "через {0} дней", + }, + "past": { + "other": "{0} дня назад", + "one": "{0} день назад", + "few": "{0} дня назад", + "many": "{0} дней назад", + }, + }, + "hour": { + "future": { + "other": "через {0} часа", + "one": "через {0} час", + "few": "через {0} часа", + "many": "через {0} часов", + }, + "past": { + "other": "{0} часа назад", + "one": "{0} час назад", + "few": "{0} часа назад", + "many": "{0} часов назад", + }, + }, + "minute": { + "future": { + "other": "через {0} минуты", + "one": "через {0} минуту", + "few": "через {0} минуты", + "many": "через {0} минут", + }, + "past": { + "other": "{0} минуты назад", + "one": "{0} минуту назад", + "few": "{0} минуты назад", + "many": "{0} минут назад", + }, + }, + "second": { + "future": { + "other": "через {0} секунды", + "one": "через {0} секунду", + "few": "через {0} секунды", + "many": "через {0} секунд", + }, + "past": { + "other": "{0} секунды назад", + "one": "{0} секунду назад", + "few": "{0} секунды назад", + "many": "{0} секунд назад", + }, + }, + }, + "day_periods": { + "midnight": "полночь", + "am": "AM", + "noon": "полдень", + "pm": "PM", + "morning1": "утра", + "afternoon1": "дня", + "evening1": "вечера", + "night1": "ночи", + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/zh/__init__.py b/pendulum/locales/zh/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/locales/zh/__init__.py diff --git a/pendulum/locales/zh/custom.py b/pendulum/locales/zh/custom.py new file mode 100644 index 0000000..eb9bb81 --- /dev/null +++ b/pendulum/locales/zh/custom.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +""" +zh custom locale file. +""" + +translations = { + # Relative time + "after": "{time}后", + "before": "{time}前", + # Date formats + "date_formats": { + "LTS": "Ah点m分s秒", + "LT": "Ah点mm分", + "LLLL": "YYYY年MMMD日ddddAh点mm分", + "LLL": "YYYY年MMMD日Ah点mm分", + "LL": "YYYY年MMMD日", + "L": "YYYY-MM-DD", + }, +} diff --git a/pendulum/locales/zh/locale.py b/pendulum/locales/zh/locale.py new file mode 100644 index 0000000..f292924 --- /dev/null +++ b/pendulum/locales/zh/locale.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .custom import translations as custom_translations + + +""" +zh locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "周日", + 1: "周一", + 2: "周二", + 3: "周三", + 4: "周四", + 5: "周五", + 6: "周六", + }, + "narrow": {0: "日", 1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六"}, + "short": {0: "周日", 1: "周一", 2: "周二", 3: "周三", 4: "周四", 5: "周五", 6: "周六"}, + "wide": { + 0: "星期日", + 1: "星期一", + 2: "星期二", + 3: "星期三", + 4: "星期四", + 5: "星期五", + 6: "星期六", + }, + }, + "months": { + "abbreviated": { + 1: "1月", + 2: "2月", + 3: "3月", + 4: "4月", + 5: "5月", + 6: "6月", + 7: "7月", + 8: "8月", + 9: "9月", + 10: "10月", + 11: "11月", + 12: "12月", + }, + "narrow": { + 1: "1", + 2: "2", + 3: "3", + 4: "4", + 5: "5", + 6: "6", + 7: "7", + 8: "8", + 9: "9", + 10: "10", + 11: "11", + 12: "12", + }, + "wide": { + 1: "一月", + 2: "二月", + 3: "三月", + 4: "四月", + 5: "五月", + 6: "六月", + 7: "七月", + 8: "八月", + 9: "九月", + 10: "十月", + 11: "十一月", + 12: "十二月", + }, + }, + "units": { + "year": {"other": "{0}年"}, + "month": {"other": "{0}个月"}, + "week": {"other": "{0}周"}, + "day": {"other": "{0}天"}, + "hour": {"other": "{0}小时"}, + "minute": {"other": "{0}分钟"}, + "second": {"other": "{0}秒钟"}, + "microsecond": {"other": "{0}微秒"}, + }, + "relative": { + "year": {"future": {"other": "{0}年后"}, "past": {"other": "{0}年前"}}, + "month": {"future": {"other": "{0}个月后"}, "past": {"other": "{0}个月前"}}, + "week": {"future": {"other": "{0}周后"}, "past": {"other": "{0}周前"}}, + "day": {"future": {"other": "{0}天后"}, "past": {"other": "{0}天前"}}, + "hour": {"future": {"other": "{0}小时后"}, "past": {"other": "{0}小时前"}}, + "minute": {"future": {"other": "{0}分钟后"}, "past": {"other": "{0}分钟前"}}, + "second": {"future": {"other": "{0}秒钟后"}, "past": {"other": "{0}秒钟前"}}, + }, + "day_periods": { + "midnight": "午夜", + "am": "上午", + "pm": "下午", + "morning1": "清晨", + "morning2": "上午", + "afternoon1": "下午", + "afternoon2": "下午", + "evening1": "晚上", + "night1": "凌晨", + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/mixins/__init__.py b/pendulum/mixins/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/pendulum/mixins/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/pendulum/mixins/default.py b/pendulum/mixins/default.py new file mode 100644 index 0000000..fdb05b0 --- /dev/null +++ b/pendulum/mixins/default.py @@ -0,0 +1,43 @@ +from ..formatting import Formatter + + +_formatter = Formatter() + + +class FormattableMixin(object): + + _formatter = _formatter + + def format(self, fmt, locale=None): + """ + Formats the instance using the given format. + + :param fmt: The format to use + :type fmt: str + + :param locale: The locale to use + :type locale: str or None + + :rtype: str + """ + return self._formatter.format(self, fmt, locale) + + def for_json(self): + """ + Methods for automatic json serialization by simplejson + + :rtype: str + """ + return str(self) + + def __format__(self, format_spec): + if len(format_spec) > 0: + if "%" in format_spec: + return self.strftime(format_spec) + + return self.format(format_spec) + + return str(self) + + def __str__(self): + return self.isoformat() diff --git a/pendulum/parser.py b/pendulum/parser.py new file mode 100644 index 0000000..0df7616 --- /dev/null +++ b/pendulum/parser.py @@ -0,0 +1,121 @@ +from __future__ import absolute_import + +import datetime +import typing + +import pendulum + +from .date import Date +from .datetime import DateTime +from .parsing import _Interval +from .parsing import parse as base_parse +from .time import Duration +from .time import Time +from .tz import UTC + + +try: + from .parsing._iso8601 import Duration as CDuration +except ImportError: + CDuration = None + + +def parse( + text, **options +): # type: (str, **typing.Any) -> typing.Union[Date, Time, DateTime, Duration] + # Use the mock now value if it exists + options["now"] = options.get("now", pendulum.get_test_now()) + + return _parse(text, **options) + + +def _parse(text, **options): + """ + Parses a string with the given options. + + :param text: The string to parse. + :type text: str + + :rtype: mixed + """ + # Handling special cases + if text == "now": + return pendulum.now() + + parsed = base_parse(text, **options) + + if isinstance(parsed, datetime.datetime): + return pendulum.datetime( + parsed.year, + parsed.month, + parsed.day, + parsed.hour, + parsed.minute, + parsed.second, + parsed.microsecond, + tz=parsed.tzinfo or options.get("tz", UTC), + ) + + if isinstance(parsed, datetime.date): + return pendulum.date(parsed.year, parsed.month, parsed.day) + + if isinstance(parsed, datetime.time): + return pendulum.time( + parsed.hour, parsed.minute, parsed.second, parsed.microsecond + ) + + if isinstance(parsed, _Interval): + if parsed.duration is not None: + duration = parsed.duration + + if parsed.start is not None: + dt = pendulum.instance(parsed.start, tz=options.get("tz", UTC)) + + return pendulum.period( + dt, + dt.add( + years=duration.years, + months=duration.months, + weeks=duration.weeks, + days=duration.remaining_days, + hours=duration.hours, + minutes=duration.minutes, + seconds=duration.remaining_seconds, + microseconds=duration.microseconds, + ), + ) + + dt = pendulum.instance(parsed.end, tz=options.get("tz", UTC)) + + return pendulum.period( + dt.subtract( + years=duration.years, + months=duration.months, + weeks=duration.weeks, + days=duration.remaining_days, + hours=duration.hours, + minutes=duration.minutes, + seconds=duration.remaining_seconds, + microseconds=duration.microseconds, + ), + dt, + ) + + return pendulum.period( + pendulum.instance(parsed.start, tz=options.get("tz", UTC)), + pendulum.instance(parsed.end, tz=options.get("tz", UTC)), + ) + + if CDuration and isinstance(parsed, CDuration): + return pendulum.duration( + years=parsed.years, + months=parsed.months, + weeks=parsed.weeks, + days=parsed.days, + hours=parsed.hours, + minutes=parsed.minutes, + seconds=parsed.seconds, + microseconds=parsed.microseconds, + ) + + return parsed diff --git a/pendulum/parsing/__init__.py b/pendulum/parsing/__init__.py new file mode 100644 index 0000000..1b5c8b9 --- /dev/null +++ b/pendulum/parsing/__init__.py @@ -0,0 +1,234 @@ +import copy +import os +import re +import struct + +from datetime import date +from datetime import datetime +from datetime import time + +from dateutil import parser + +from .exceptions import ParserError + + +with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1" + +try: + if not with_extensions or struct.calcsize("P") == 4: + raise ImportError() + + from ._iso8601 import parse_iso8601 +except ImportError: + from .iso8601 import parse_iso8601 + + +COMMON = re.compile( + # Date (optional) + "^" + "(?P<date>" + " (?P<classic>" # Classic date (YYYY-MM-DD) + r" (?P<year>\d{4})" # Year + " (?P<monthday>" + r" (?P<monthsep>[/:])?(?P<month>\d{2})" # Month (optional) + r" ((?P<daysep>[/:])?(?P<day>\d{2}))" # Day (optional) + " )?" + " )" + ")?" + # Time (optional) + "(?P<time>" + r" (?P<timesep>\ )?" # Separator (space) + r" (?P<hour>\d{1,2}):(?P<minute>\d{1,2})?(?::(?P<second>\d{1,2}))?" # HH:mm:ss (optional mm and ss) + # Subsecond part (optional) + " (?P<subsecondsection>" + " (?:[.|,])" # Subsecond separator (optional) + r" (?P<subsecond>\d{1,9})" # Subsecond + " )?" + ")?" + "$", + re.VERBOSE, +) + + +DEFAULT_OPTIONS = { + "day_first": False, + "year_first": True, + "strict": True, + "exact": False, + "now": None, +} + + +def parse(text, **options): + """ + Parses a string with the given options. + + :param text: The string to parse. + :type text: str + + :rtype: Parsed + """ + _options = copy.copy(DEFAULT_OPTIONS) + _options.update(options) + + return _normalize(_parse(text, **_options), **_options) + + +def _normalize(parsed, **options): + """ + Normalizes the parsed element. + + :param parsed: The parsed elements. + :type parsed: Parsed + + :rtype: Parsed + """ + if options.get("exact"): + return parsed + + if isinstance(parsed, time): + now = options["now"] or datetime.now() + + return datetime( + now.year, + now.month, + now.day, + parsed.hour, + parsed.minute, + parsed.second, + parsed.microsecond, + ) + elif isinstance(parsed, date) and not isinstance(parsed, datetime): + return datetime(parsed.year, parsed.month, parsed.day) + + return parsed + + +def _parse(text, **options): + # Trying to parse ISO8601 + try: + return parse_iso8601(text) + except ValueError: + pass + + try: + return _parse_iso8601_interval(text) + except ValueError: + pass + + try: + return _parse_common(text, **options) + except ParserError: + pass + + # We couldn't parse the string + # so we fallback on the dateutil parser + # If not strict + if options.get("strict", True): + raise ParserError("Unable to parse string [{}]".format(text)) + + try: + dt = parser.parse( + text, dayfirst=options["day_first"], yearfirst=options["year_first"] + ) + except ValueError: + raise ParserError("Invalid date string: {}".format(text)) + + return dt + + +def _parse_common(text, **options): + """ + Tries to parse the string as a common datetime format. + + :param text: The string to parse. + :type text: str + + :rtype: dict or None + """ + m = COMMON.match(text) + has_date = False + year = 0 + month = 1 + day = 1 + + if not m: + raise ParserError("Invalid datetime string") + + if m.group("date"): + # A date has been specified + has_date = True + + year = int(m.group("year")) + + if not m.group("monthday"): + # No month and day + month = 1 + day = 1 + else: + if options["day_first"]: + month = int(m.group("day")) + day = int(m.group("month")) + else: + month = int(m.group("month")) + day = int(m.group("day")) + + if not m.group("time"): + return date(year, month, day) + + # Grabbing hh:mm:ss + hour = int(m.group("hour")) + + minute = int(m.group("minute")) + + if m.group("second"): + second = int(m.group("second")) + else: + second = 0 + + # Grabbing subseconds, if any + microsecond = 0 + if m.group("subsecondsection"): + # Limiting to 6 chars + subsecond = m.group("subsecond")[:6] + + microsecond = int("{:0<6}".format(subsecond)) + + if has_date: + return datetime(year, month, day, hour, minute, second, microsecond) + + return time(hour, minute, second, microsecond) + + +class _Interval: + """ + Special class to handle ISO 8601 intervals + """ + + def __init__(self, start=None, end=None, duration=None): + self.start = start + self.end = end + self.duration = duration + + +def _parse_iso8601_interval(text): + if "/" not in text: + raise ParserError("Invalid interval") + + first, last = text.split("/") + start = end = duration = None + + if first[0] == "P": + # duration/end + duration = parse_iso8601(first) + end = parse_iso8601(last) + elif last[0] == "P": + # start/duration + start = parse_iso8601(first) + duration = parse_iso8601(last) + else: + # start/end + start = parse_iso8601(first) + end = parse_iso8601(last) + + return _Interval(start, end, duration) diff --git a/pendulum/parsing/_iso8601.c b/pendulum/parsing/_iso8601.c new file mode 100644 index 0000000..41c66fa --- /dev/null +++ b/pendulum/parsing/_iso8601.c @@ -0,0 +1,1371 @@ +/* ------------------------------------------------------------------------- */ + +#include <Python.h> +#include <datetime.h> +#include <structmember.h> +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <stdint.h> + +#ifndef PyVarObject_HEAD_INIT +#define PyVarObject_HEAD_INIT(type, size) PyObject_HEAD_INIT(type) size, +#endif + + +/* ------------------------------------------------------------------------- */ + +#define EPOCH_YEAR 1970 + +#define DAYS_PER_N_YEAR 365 +#define DAYS_PER_L_YEAR 366 + +#define USECS_PER_SEC 1000000 + +#define SECS_PER_MIN 60 +#define SECS_PER_HOUR (60 * SECS_PER_MIN) +#define SECS_PER_DAY (SECS_PER_HOUR * 24) + +// 400-year chunks always have 146097 days (20871 weeks). +#define DAYS_PER_400_YEARS 146097L +#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)SECS_PER_DAY) + +// The number of seconds in an aligned 100-year chunk, for those that +// do not begin with a leap year and those that do respectively. +const int64_t SECS_PER_100_YEARS[2] = { + (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY, + (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * DAYS_PER_L_YEAR) * SECS_PER_DAY +}; + +// The number of seconds in an aligned 4-year chunk, for those that +// do not begin with a leap year and those that do respectively. +const int32_t SECS_PER_4_YEARS[2] = { + (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY, + (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY +}; + +// The number of seconds in non-leap and leap years respectively. +const int32_t SECS_PER_YEAR[2] = { + DAYS_PER_N_YEAR * SECS_PER_DAY, + DAYS_PER_L_YEAR * SECS_PER_DAY +}; + +#define MONTHS_PER_YEAR 12 + +// The month lengths in non-leap and leap years respectively. +const int32_t DAYS_PER_MONTHS[2][13] = { + {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, + {-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} +}; + +// The day offsets of the beginning of each (1-based) month in non-leap +// and leap years respectively. +// For example, in a leap year there are 335 days before December. +const int32_t MONTHS_OFFSETS[2][14] = { + {-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}, + {-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366} +}; + +const int DAY_OF_WEEK_TABLE[12] = { + 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 +}; + +#define TM_SUNDAY 0 +#define TM_MONDAY 1 +#define TM_TUESDAY 2 +#define TM_WEDNESDAY 3 +#define TM_THURSDAY 4 +#define TM_FRIDAY 5 +#define TM_SATURDAY 6 + +#define TM_JANUARY 0 +#define TM_FEBRUARY 1 +#define TM_MARCH 2 +#define TM_APRIL 3 +#define TM_MAY 4 +#define TM_JUNE 5 +#define TM_JULY 6 +#define TM_AUGUST 7 +#define TM_SEPTEMBER 8 +#define TM_OCTOBER 9 +#define TM_NOVEMBER 10 +#define TM_DECEMBER 11 + +// Parsing errors +const int PARSER_INVALID_ISO8601 = 0; +const int PARSER_INVALID_DATE = 1; +const int PARSER_INVALID_TIME = 2; +const int PARSER_INVALID_WEEK_DATE = 3; +const int PARSER_INVALID_WEEK_NUMBER = 4; +const int PARSER_INVALID_WEEKDAY_NUMBER = 5; +const int PARSER_INVALID_ORDINAL_DAY_FOR_YEAR = 6; +const int PARSER_INVALID_MONTH_OR_DAY = 7; +const int PARSER_INVALID_MONTH = 8; +const int PARSER_INVALID_DAY_FOR_MONTH = 9; +const int PARSER_INVALID_HOUR = 10; +const int PARSER_INVALID_MINUTE = 11; +const int PARSER_INVALID_SECOND = 12; +const int PARSER_INVALID_SUBSECOND = 13; +const int PARSER_INVALID_TZ_OFFSET = 14; +const int PARSER_INVALID_DURATION = 15; +const int PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED = 16; + +const char PARSER_ERRORS[17][80] = { + "Invalid ISO 8601 string", + "Invalid date", + "Invalid time", + "Invalid week date", + "Invalid week number", + "Invalid weekday number", + "Invalid ordinal day for year", + "Invalid month and/or day", + "Invalid month", + "Invalid day for month", + "Invalid hour", + "Invalid minute", + "Invalid second", + "Invalid subsecond", + "Invalid timezone offset", + "Invalid duration", + "Float years and months are not supported" +}; + +/* ------------------------------------------------------------------------- */ + + +int p(int y) { + return y + y/4 - y/100 + y/400; +} + +int is_leap(int year) { + return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); +} + +int week_day(int year, int month, int day) { + int y; + int w; + + y = year - (month < 3); + + w = (p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7; + + if (!w) { + w = 7; + } + + return w; +} + +int days_in_year(int year) { + if (is_leap(year)) { + return DAYS_PER_L_YEAR; + } + + return DAYS_PER_N_YEAR; +} + +int is_long_year(int year) { + return (p(year) % 7 == 4) || (p(year - 1) % 7 == 3); +} + + +/* ------------------------ Custom Types ------------------------------- */ + + +/* + * class FixedOffset(tzinfo): + */ +typedef struct { + PyObject_HEAD + int offset; + char *tzname; +} FixedOffset; + +/* + * def __init__(self, offset): + * self.offset = offset +*/ +static int FixedOffset_init(FixedOffset *self, PyObject *args, PyObject *kwargs) { + int offset; + char *tzname = NULL; + + static char *kwlist[] = {"offset", "tzname", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|s", kwlist, &offset, &tzname)) + return -1; + + self->offset = offset; + self->tzname = tzname; + + return 0; +} + +/* + * def utcoffset(self, dt): + * return timedelta(seconds=self.offset * 60) + */ +static PyObject *FixedOffset_utcoffset(FixedOffset *self, PyObject *args) { + return PyDelta_FromDSU(0, self->offset, 0); +} + +/* + * def dst(self, dt): + * return timedelta(seconds=self.offset * 60) + */ +static PyObject *FixedOffset_dst(FixedOffset *self, PyObject *args) { + return PyDelta_FromDSU(0, self->offset, 0); +} + +/* + * def tzname(self, dt): + * sign = '+' + * if self.offset < 0: + * sign = '-' + * return "%s%d:%d" % (sign, self.offset / 60, self.offset % 60) + */ +static PyObject *FixedOffset_tzname(FixedOffset *self, PyObject *args) { + if (self->tzname != NULL) { + return PyUnicode_FromString(self->tzname); + } + + char tzname_[7] = {0}; + char sign = '+'; + int offset = self->offset; + + if (offset < 0) { + sign = '-'; + offset *= -1; + } + + sprintf( + tzname_, + "%c%02d:%02d", + sign, + offset / SECS_PER_HOUR, + offset / SECS_PER_MIN % SECS_PER_MIN + ); + + return PyUnicode_FromString(tzname_); +} + +/* + * def __repr__(self): + * return self.tzname() + */ +static PyObject *FixedOffset_repr(FixedOffset *self) { + return FixedOffset_tzname(self, NULL); +} + +/* + * Class member / class attributes + */ +static PyMemberDef FixedOffset_members[] = { + {"offset", T_INT, offsetof(FixedOffset, offset), 0, "UTC offset"}, + {NULL} +}; + +/* + * Class methods + */ +static PyMethodDef FixedOffset_methods[] = { + {"utcoffset", (PyCFunction)FixedOffset_utcoffset, METH_VARARGS, ""}, + {"dst", (PyCFunction)FixedOffset_dst, METH_VARARGS, ""}, + {"tzname", (PyCFunction)FixedOffset_tzname, METH_VARARGS, ""}, + {NULL} +}; + +static PyTypeObject FixedOffset_type = { + PyVarObject_HEAD_INIT(NULL, 0) + "FixedOffset_type", /* tp_name */ + sizeof(FixedOffset), /* tp_basicsize */ + 0, /* tp_itemsize */ + 0, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_as_async */ + (reprfunc)FixedOffset_repr, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + (reprfunc)FixedOffset_repr, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */ + "TZInfo with fixed offset", /* tp_doc */ +}; + +/* + * Instantiate new FixedOffset_type object + * Skip overhead of calling PyObject_New and PyObject_Init. + * Directly allocate object. + */ +static PyObject *new_fixed_offset_ex(int offset, char *name, PyTypeObject *type) { + FixedOffset *self = (FixedOffset *) (type->tp_alloc(type, 0)); + + if (self != NULL) + self->offset = offset; + self->tzname = name; + + return (PyObject *) self; +} + +#define new_fixed_offset(offset, name) new_fixed_offset_ex(offset, name, &FixedOffset_type) + + +/* + * class Duration(): + */ +typedef struct { + PyObject_HEAD + int years; + int months; + int weeks; + int days; + int hours; + int minutes; + int seconds; + int microseconds; +} Duration; + +/* + * def __init__(self, years, months, days, hours, minutes, seconds, microseconds): + * self.years = years + * # ... +*/ +static int Duration_init(Duration *self, PyObject *args, PyObject *kwargs) { + int years; + int months; + int weeks; + int days; + int hours; + int minutes; + int seconds; + int microseconds; + + if (!PyArg_ParseTuple(args, "iiiiiiii", &years, &months, &weeks, &days, &hours, &minutes, &seconds, µseconds)) + return -1; + + self->years = years; + self->months = months; + self->weeks = weeks; + self->days = days; + self->hours = hours; + self->minutes = minutes; + self->seconds = seconds; + self->microseconds = microseconds; + + return 0; +} + +/* + * def __repr__(self): + * return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format( + * self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds + * ) + */ +static PyObject *Duration_repr(Duration *self) { + char repr[82] = {0}; + + sprintf( + repr, + "%d years %d months %d weeks %d days %d hours %d minutes %d seconds %d microseconds", + self->years, + self->months, + self->weeks, + self->days, + self->hours, + self->minutes, + self->seconds, + self->microseconds + ); + + return PyUnicode_FromString(repr); +} + +/* + * Instantiate new Duration_type object + * Skip overhead of calling PyObject_New and PyObject_Init. + * Directly allocate object. + */ +static PyObject *new_duration_ex(int years, int months, int weeks, int days, int hours, int minutes, int seconds, int microseconds, PyTypeObject *type) { + Duration *self = (Duration *) (type->tp_alloc(type, 0)); + + if (self != NULL) { + self->years = years; + self->months = months; + self->weeks = weeks; + self->days = days; + self->hours = hours; + self->minutes = minutes; + self->seconds = seconds; + self->microseconds = microseconds; + } + + return (PyObject *) self; +} + +/* + * Class member / class attributes + */ +static PyMemberDef Duration_members[] = { + {"years", T_INT, offsetof(Duration, years), 0, "years in duration"}, + {"months", T_INT, offsetof(Duration, months), 0, "months in duration"}, + {"weeks", T_INT, offsetof(Duration, weeks), 0, "weeks in duration"}, + {"days", T_INT, offsetof(Duration, days), 0, "days in duration"}, + {"remaining_days", T_INT, offsetof(Duration, days), 0, "days in duration"}, + {"hours", T_INT, offsetof(Duration, hours), 0, "hours in duration"}, + {"minutes", T_INT, offsetof(Duration, minutes), 0, "minutes in duration"}, + {"seconds", T_INT, offsetof(Duration, seconds), 0, "seconds in duration"}, + {"remaining_seconds", T_INT, offsetof(Duration, seconds), 0, "seconds in duration"}, + {"microseconds", T_INT, offsetof(Duration, microseconds), 0, "microseconds in duration"}, + {NULL} +}; + +static PyTypeObject Duration_type = { + PyVarObject_HEAD_INIT(NULL, 0) + "Duration", /* tp_name */ + sizeof(Duration), /* tp_basicsize */ + 0, /* tp_itemsize */ + 0, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_as_async */ + (reprfunc)Duration_repr, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + (reprfunc)Duration_repr, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */ + "Duration", /* tp_doc */ +}; + +#define new_duration(years, months, weeks, days, hours, minutes, seconds, microseconds) new_duration_ex(years, months, weeks, days, hours, minutes, seconds, microseconds, &Duration_type) + +typedef struct { + int is_date; + int is_time; + int is_datetime; + int is_duration; + int is_period; + int ambiguous; + int year; + int month; + int day; + int hour; + int minute; + int second; + int microsecond; + int offset; + int has_offset; + char *tzname; + int years; + int months; + int weeks; + int days; + int hours; + int minutes; + int seconds; + int microseconds; + int error; +} Parsed; + + +Parsed* new_parsed() { + Parsed *parsed; + + if((parsed = malloc(sizeof *parsed)) != NULL) { + parsed->is_date = 0; + parsed->is_time = 0; + parsed->is_datetime = 0; + parsed->is_duration = 0; + parsed->is_period = 0; + + parsed->ambiguous = 0; + parsed->year = 0; + parsed->month = 1; + parsed->day = 1; + parsed->hour = 0; + parsed->minute = 0; + parsed->second = 0; + parsed->microsecond = 0; + parsed->offset = 0; + parsed->has_offset = 0; + parsed->tzname = NULL; + + parsed->years = 0; + parsed->months = 0; + parsed->weeks = 0; + parsed->days = 0; + parsed->hours = 0; + parsed->minutes = 0; + parsed->seconds = 0; + parsed->microseconds = 0; + + parsed->error = -1; + } + + return parsed; +} + + +/* -------------------------- Functions --------------------------*/ + +Parsed* _parse_iso8601_datetime(char *str, Parsed *parsed) { + char* c; + int monthday = 0; + int week = 0; + int weekday = 1; + int ordinal; + int tz_sign = 0; + int leap = 0; + int separators = 0; + int time = 0; + int has_hour = 0; + int i; + int j; + + // Assuming date only for now + parsed->is_date = 1; + + c = str; + + for (i = 0; i < 4; i++) { + if (*c >= '0' && *c <= '9') { + parsed->year = 10 * parsed->year + *c++ - '0'; + } else { + parsed->error = PARSER_INVALID_ISO8601; + + return NULL; + } + } + + leap = is_leap(parsed->year); + + // Optional separator + if (*c == '-') { + separators++; + c++; + } + + // Checking for week dates + if (*c == 'W') { + c++; + + i = 0; + while (*c != '\0' && *c != ' ' && *c != 'T') { + if (*c == '-') { + separators++; + c++; + continue; + } + + week = 10 * week + *c++ - '0'; + + i++; + } + + switch (i) { + case 2: + // Only week number + break; + case 3: + // Week with weekday + if (!(separators == 0 || separators == 2)) { + // We should have 2 or no separator + parsed->error = PARSER_INVALID_WEEK_DATE; + + return NULL; + } + + weekday = week % 10; + week /= 10; + + break; + default: + // Any other case is wrong + parsed->error = PARSER_INVALID_WEEK_DATE; + + return NULL; + } + + // Checks + if (week > 53 || (week > 52 && !is_long_year(parsed->year))) { + parsed->error = PARSER_INVALID_WEEK_NUMBER; + + return NULL; + } + + if (weekday > 7) { + parsed->error = PARSER_INVALID_WEEKDAY_NUMBER; + + return NULL; + } + + // Calculating ordinal day + ordinal = week * 7 + weekday - (week_day(parsed->year, 1, 4) + 3); + + if (ordinal < 1) { + // Previous year + ordinal += days_in_year(parsed->year - 1); + parsed->year -= 1; + leap = is_leap(parsed->year); + } + + if (ordinal > days_in_year(parsed->year)) { + // Next year + ordinal -= days_in_year(parsed->year); + parsed->year += 1; + leap = is_leap(parsed->year); + } + + for (j = 1; j < 14; j++) { + if (ordinal <= MONTHS_OFFSETS[leap][j]) { + parsed->day = ordinal - MONTHS_OFFSETS[leap][j - 1]; + parsed->month = j - 1; + + break; + } + } + } else { + // At this point we need to check the number + // of characters until the end of the date part + // (or the end of the string). + // + // If two, we have only a month if there is a separator, it may be a time otherwise. + // If three, we have an ordinal date. + // If four, we have a complete date + i = 0; + while (*c != '\0' && *c != ' ' && *c != 'T') { + if (*c == '-') { + separators++; + c++; + continue; + } + + if (!(*c >= '0' && *c <='9')) { + parsed->error = PARSER_INVALID_DATE; + + return NULL; + } + + monthday = 10 * monthday + *c++ - '0'; + + i++; + } + + switch (i) { + case 0: + // No month/day specified (only a year) + break; + case 2: + if (!separators) { + // The date looks like 201207 + // which is invalid for a date + // But it might be a time in the form hhmmss + parsed->ambiguous = 1; + } else if (separators > 1) { + parsed->error = PARSER_INVALID_DATE; + + return NULL; + } + + parsed->month = monthday; + break; + case 3: + // Ordinal day + if (separators > 1) { + parsed->error = PARSER_INVALID_DATE; + + return NULL; + } + + if (monthday < 1 || monthday > MONTHS_OFFSETS[leap][13]) { + parsed->error = PARSER_INVALID_ORDINAL_DAY_FOR_YEAR; + + return NULL; + } + + for (j = 1; j < 14; j++) { + if (monthday <= MONTHS_OFFSETS[leap][j]) { + parsed->day = monthday - MONTHS_OFFSETS[leap][j - 1]; + parsed->month = j - 1; + + break; + } + } + + break; + case 4: + // Month and day + parsed->month = monthday / 100; + parsed->day = monthday % 100; + + break; + default: + parsed->error = PARSER_INVALID_MONTH_OR_DAY; + + return NULL; + } + } + + // Checks + if (separators && !monthday && !week) { + parsed->error = PARSER_INVALID_DATE; + + return NULL; + } + + if (parsed->month > 12) { + parsed->error = PARSER_INVALID_MONTH; + + return NULL; + } + + if (parsed->day > DAYS_PER_MONTHS[leap][parsed->month]) { + parsed->error = PARSER_INVALID_DAY_FOR_MONTH; + + return NULL; + } + + separators = 0; + if (*c == 'T' || *c == ' ') { + if (parsed->ambiguous) { + parsed->error = PARSER_INVALID_DATE; + + return NULL; + } + + // We have time so we have a datetime + parsed->is_datetime = 1; + parsed->is_date = 0; + + c++; + + // Grabbing time information + i = 0; + while (*c != '\0' && *c != '.' && *c != ',' && *c != 'Z' && *c != '+' && *c != '-') { + if (*c == ':') { + separators++; + c++; + continue; + } + + if (!(*c >= '0' && *c <='9')) { + parsed->error = PARSER_INVALID_TIME; + + return NULL; + } + + time = 10 * time + *c++ - '0'; + i++; + } + + switch (i) { + case 2: + // Hours only + if (separators > 0) { + // Extraneous separators + parsed->error = PARSER_INVALID_TIME; + + return NULL; + } + + parsed->hour = time; + has_hour = 1; + break; + case 4: + // Hours and minutes + if (separators > 1) { + // Extraneous separators + parsed->error = PARSER_INVALID_TIME; + + return NULL; + } + + parsed->hour = time / 100; + parsed->minute = time % 100; + has_hour = 1; + break; + case 6: + // Hours, minutes and seconds + if (!(separators == 0 || separators == 2)) { + // We should have either two separators or none + parsed->error = PARSER_INVALID_TIME; + + return NULL; + } + + parsed->hour = time / 10000; + parsed->minute = time / 100 % 100; + parsed->second = time % 100; + has_hour = 1; + break; + default: + // Any other case is wrong + parsed->error = PARSER_INVALID_TIME; + + return NULL; + } + + // Checks + if (parsed->hour > 23) { + parsed->error = PARSER_INVALID_HOUR; + + return NULL; + } + + if (parsed->minute > 59) { + parsed->error = PARSER_INVALID_MINUTE; + + return NULL; + } + + if (parsed->second > 59) { + parsed->error = PARSER_INVALID_SECOND; + + return NULL; + } + + // Subsecond + if (*c == '.' || *c == ',') { + c++; + + time = 0; + i = 0; + while (*c != '\0' && *c != 'Z' && *c != '+' && *c != '-') { + if (!(*c >= '0' && *c <='9')) { + parsed->error = PARSER_INVALID_SUBSECOND; + + return NULL; + } + + time = 10 * time + *c++ - '0'; + i++; + } + + // adjust to microseconds + if (i > 6) { + parsed->microsecond = time / pow(10, i - 6); + } else if (i <= 6) { + parsed->microsecond = time * pow(10, 6 - i); + } + } + + // Timezone + if (*c == 'Z') { + parsed->has_offset = 1; + parsed->tzname = "UTC"; + c++; + } else if (*c == '+' || *c == '-') { + tz_sign = 1; + if (*c == '-') { + tz_sign = -1; + } + + parsed->has_offset = 1; + c++; + + i = 0; + time = 0; + separators = 0; + while (*c != '\0') { + if (*c == ':') { + separators++; + c++; + continue; + } + + if (!(*c >= '0' && *c <= '9')) { + parsed->error = PARSER_INVALID_TZ_OFFSET; + + return NULL; + } + + time = 10 * time + *c++ - '0'; + i++; + } + + switch (i) { + case 2: + // hh Format + if (separators) { + // Extraneous separators + parsed->error = PARSER_INVALID_TZ_OFFSET; + + return NULL; + } + + parsed->offset = tz_sign * (time * 3600); + break; + case 4: + // hhmm Format + if (separators > 1) { + // Extraneous separators + parsed->error = PARSER_INVALID_TZ_OFFSET; + + return NULL; + } + + parsed->offset = tz_sign * ((time / 100 * 3600) + (time % 100 * 60)); + break; + default: + // Wrong format + parsed->error = PARSER_INVALID_TZ_OFFSET; + + return NULL; + } + } + } + + // At this point we should be at the end of the string + // If not, the string is invalid + if (*c != '\0') { + parsed->error = PARSER_INVALID_ISO8601; + + return NULL; + } + + return parsed; +} + + +Parsed* _parse_iso8601_duration(char *str, Parsed *parsed) { + char* c; + int value = 0; + int grabbed = 0; + int in_time = 0; + int in_fraction = 0; + int fraction_length = 0; + int has_fractional = 0; + int fraction = 0; + int has_ymd = 0; + int has_week = 0; + int has_year = 0; + int has_month = 0; + int has_day = 0; + int has_hour = 0; + int has_minute = 0; + int has_second = 0; + + c = str; + + // Removing P operator + c++; + + parsed->is_duration = 1; + + for (; *c != '\0'; c++) { + switch (*c) { + case 'Y': + if (!grabbed || in_time || has_week || has_ymd) { + // No value grabbed + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + if (fraction) { + parsed->error = PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED; + + return NULL; + } + + parsed->years = value; + + grabbed = 0; + value = 0; + fraction = 0; + in_fraction = 0; + has_ymd = 1; + has_year = 1; + + break; + case 'M': + if (!grabbed || has_week) { + // No value grabbed + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + if (in_time) { + if (has_second) { + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + if (has_fractional) { + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + parsed->minutes = value; + if (fraction) { + parsed->seconds = fraction * 6; + has_fractional = 1; + } + + has_minute = 1; + } else { + if (fraction) { + parsed->error = PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED; + + return NULL; + } + + if (has_month || has_day) { + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + parsed->months = value; + has_ymd = 1; + has_month = 1; + } + + grabbed = 0; + value = 0; + fraction = 0; + in_fraction = 0; + + break; + case 'D': + if (!grabbed || in_time || has_week) { + // No value grabbed + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + if (has_day) { + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + parsed->days = value; + if (fraction) { + parsed->hours = fraction * 2.4; + has_fractional = 1; + } + + grabbed = 0; + value = 0; + fraction = 0; + in_fraction = 0; + has_ymd = 1; + has_day = 1; + + break; + case 'T': + if (grabbed) { + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + in_time = 1; + + break; + case 'H': + if (!grabbed || !in_time || has_week) { + // No value grabbed + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + if (has_hour || has_second || has_minute) { + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + if (has_fractional) { + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + parsed->hours = value; + if (fraction) { + parsed->minutes = fraction * 6; + has_fractional = 1; + } + + grabbed = 0; + value = 0; + fraction = 0; + in_fraction = 0; + has_hour = 1; + + break; + case 'S': + if (!grabbed || !in_time || has_week) { + // No value grabbed + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + if (has_second) { + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + if (has_fractional) { + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + if (fraction) { + parsed->seconds = value; + if (fraction_length > 6) { + parsed->microseconds = fraction / pow(10, fraction_length - 6); + } else { + parsed->microseconds = fraction * pow(10, 6 - fraction_length); + } + has_fractional = 1; + } else { + parsed->seconds = value; + } + + grabbed = 0; + value = 0; + fraction = 0; + in_fraction = 0; + has_second = 1; + + break; + case 'W': + if (!grabbed || in_time || has_ymd) { + // No value grabbed + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + parsed->weeks = value; + if (fraction) { + float days; + days = fraction * 0.7; + parsed->hours = (int) ((days - (int) days) * 24); + parsed->days = (int) days; + } + + grabbed = 0; + value = 0; + fraction = 0; + in_fraction = 0; + has_week = 1; + + break; + case '.': + if (!grabbed || has_fractional) { + // No value grabbed + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + in_fraction = 1; + + break; + case ',': + if (!grabbed || has_fractional) { + // No value grabbed + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + + in_fraction = 1; + + break; + default: + if (*c >= '0' && *c <='9') { + if (in_fraction) { + fraction = 10 * fraction + *c - '0'; + fraction_length++; + } else { + value = 10 * value + *c - '0'; + grabbed = 1; + } + break; + } + + parsed->error = PARSER_INVALID_DURATION; + + return NULL; + } + } + + return parsed; +} + + +PyObject* parse_iso8601(PyObject *self, PyObject *args) { + char* str; + PyObject *obj; + PyObject *tzinfo; + Parsed *parsed = new_parsed(); + + if (!PyArg_ParseTuple(args, "s", &str)) { + PyErr_SetString( + PyExc_ValueError, "Invalid parameters" + ); + return NULL; + } + + if (*str == 'P') { + // Duration (or interval) + if (_parse_iso8601_duration(str, parsed) == NULL) { + PyErr_SetString( + PyExc_ValueError, PARSER_ERRORS[parsed->error] + ); + + return NULL; + } + } else if (_parse_iso8601_datetime(str, parsed) == NULL) { + PyErr_SetString( + PyExc_ValueError, PARSER_ERRORS[parsed->error] + ); + + return NULL; + } + + if (parsed->is_date) { + // Date only + if (parsed->ambiguous) { + // We can "safely" assume that the ambiguous + // date was actually a time in the form hhmmss + parsed->hour = parsed->year / 100; + parsed->minute = parsed->year % 100; + parsed->second = parsed->month; + + obj = PyDateTimeAPI->Time_FromTime( + parsed->hour, parsed->minute, parsed->second, parsed->microsecond, + Py_BuildValue(""), + PyDateTimeAPI->TimeType + ); + } else { + obj = PyDateTimeAPI->Date_FromDate( + parsed->year, parsed->month, parsed->day, + PyDateTimeAPI->DateType + ); + } + } else if (parsed->is_datetime) { + if (!parsed->has_offset) { + tzinfo = Py_BuildValue(""); + } else { + tzinfo = new_fixed_offset(parsed->offset, parsed->tzname); + } + + obj = PyDateTimeAPI->DateTime_FromDateAndTime( + parsed->year, + parsed->month, + parsed->day, + parsed->hour, + parsed->minute, + parsed->second, + parsed->microsecond, + tzinfo, + PyDateTimeAPI->DateTimeType + ); + + Py_DECREF(tzinfo); + } else if (parsed->is_duration) { + obj = new_duration( + parsed->years, parsed->months, parsed->weeks, parsed->days, + parsed->hours, parsed->minutes, parsed->seconds, parsed->microseconds + ); + } else { + return NULL; + } + + free(parsed); + + return obj; +} + + +/* ------------------------------------------------------------------------- */ + +static PyMethodDef helpers_methods[] = { + { + "parse_iso8601", + (PyCFunction) parse_iso8601, + METH_VARARGS, + PyDoc_STR("Parses a ISO8601 string into a tuple.") + }, + {NULL} +}; + + +/* ------------------------------------------------------------------------- */ + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_iso8601", + NULL, + -1, + helpers_methods, + NULL, + NULL, + NULL, + NULL, +}; + +PyMODINIT_FUNC +PyInit__iso8601(void) +{ + PyObject *module; + + PyDateTime_IMPORT; + + module = PyModule_Create(&moduledef); + + if (module == NULL) + return NULL; + + // FixedOffset declaration + FixedOffset_type.tp_new = PyType_GenericNew; + FixedOffset_type.tp_base = PyDateTimeAPI->TZInfoType; + FixedOffset_type.tp_methods = FixedOffset_methods; + FixedOffset_type.tp_members = FixedOffset_members; + FixedOffset_type.tp_init = (initproc)FixedOffset_init; + + if (PyType_Ready(&FixedOffset_type) < 0) + return NULL; + + // Duration declaration + Duration_type.tp_new = PyType_GenericNew; + Duration_type.tp_members = Duration_members; + Duration_type.tp_init = (initproc)Duration_init; + + if (PyType_Ready(&Duration_type) < 0) + return NULL; + + Py_INCREF(&FixedOffset_type); + Py_INCREF(&Duration_type); + + PyModule_AddObject(module, "TZFixedOffset", (PyObject *)&FixedOffset_type); + PyModule_AddObject(module, "Duration", (PyObject *)&Duration_type); + + return module; +} diff --git a/pendulum/parsing/exceptions/__init__.py b/pendulum/parsing/exceptions/__init__.py new file mode 100644 index 0000000..9b4ea71 --- /dev/null +++ b/pendulum/parsing/exceptions/__init__.py @@ -0,0 +1,3 @@ +class ParserError(ValueError): + + pass diff --git a/pendulum/parsing/iso8601.py b/pendulum/parsing/iso8601.py new file mode 100644 index 0000000..2d8f222 --- /dev/null +++ b/pendulum/parsing/iso8601.py @@ -0,0 +1,447 @@ +from __future__ import division + +import datetime +import re + +from ..constants import HOURS_PER_DAY +from ..constants import MINUTES_PER_HOUR +from ..constants import MONTHS_OFFSETS +from ..constants import SECONDS_PER_MINUTE +from ..duration import Duration +from ..helpers import days_in_year +from ..helpers import is_leap +from ..helpers import is_long_year +from ..helpers import week_day +from ..tz.timezone import UTC +from ..tz.timezone import FixedTimezone +from .exceptions import ParserError + + +ISO8601_DT = re.compile( + # Date (optional) + "^" + "(?P<date>" + " (?P<classic>" # Classic date (YYYY-MM-DD) or ordinal (YYYY-DDD) + r" (?P<year>\d{4})" # Year + " (?P<monthday>" + r" (?P<monthsep>-)?(?P<month>\d{2})" # Month (optional) + r" ((?P<daysep>-)?(?P<day>\d{1,2}))?" # Day (optional) + " )?" + " )" + " |" + " (?P<isocalendar>" # Calendar date (2016-W05 or 2016-W05-5) + r" (?P<isoyear>\d{4})" # Year + " (?P<weeksep>-)?" # Separator (optional) + " W" # W separator + r" (?P<isoweek>\d{2})" # Week number + " (?P<weekdaysep>-)?" # Separator (optional) + r" (?P<isoweekday>\d)?" # Weekday (optional) + " )" + ")?" + # Time (optional) + "(?P<time>" + r" (?P<timesep>[T\ ])?" # Separator (T or space) + r" (?P<hour>\d{1,2})(?P<minsep>:)?(?P<minute>\d{1,2})?(?P<secsep>:)?(?P<second>\d{1,2})?" # HH:mm:ss (optional mm and ss) + # Subsecond part (optional) + " (?P<subsecondsection>" + " (?:[.,])" # Subsecond separator (optional) + r" (?P<subsecond>\d{1,9})" # Subsecond + " )?" + # Timezone offset + " (?P<tz>" + r" (?:[-+])\d{2}:?(?:\d{2})?|Z" # Offset (+HH:mm or +HHmm or +HH or Z) + " )?" + ")?" + "$", + re.VERBOSE, +) + + +ISO8601_DURATION = re.compile( + "^P" # Duration P indicator + # Years, months and days (optional) + "(?P<w>" + r" (?P<weeks>\d+(?:[.,]\d+)?W)" + ")?" + "(?P<ymd>" + r" (?P<years>\d+(?:[.,]\d+)?Y)?" + r" (?P<months>\d+(?:[.,]\d+)?M)?" + r" (?P<days>\d+(?:[.,]\d+)?D)?" + ")?" + "(?P<hms>" + " (?P<timesep>T)" # Separator (T) + r" (?P<hours>\d+(?:[.,]\d+)?H)?" + r" (?P<minutes>\d+(?:[.,]\d+)?M)?" + r" (?P<seconds>\d+(?:[.,]\d+)?S)?" + ")?" + "$", + re.VERBOSE, +) + + +def parse_iso8601(text): + """ + ISO 8601 compliant parser. + + :param text: The string to parse + :type text: str + + :rtype: datetime.datetime or datetime.time or datetime.date + """ + parsed = _parse_iso8601_duration(text) + if parsed is not None: + return parsed + + m = ISO8601_DT.match(text) + if not m: + raise ParserError("Invalid ISO 8601 string") + + ambiguous_date = False + is_date = False + is_time = False + year = 0 + month = 1 + day = 1 + minute = 0 + second = 0 + microsecond = 0 + tzinfo = None + + if m: + if m.group("date"): + # A date has been specified + is_date = True + + if m.group("isocalendar"): + # We have a ISO 8601 string defined + # by week number + if ( + m.group("weeksep") + and not m.group("weekdaysep") + and m.group("isoweekday") + ): + raise ParserError("Invalid date string: {}".format(text)) + + if not m.group("weeksep") and m.group("weekdaysep"): + raise ParserError("Invalid date string: {}".format(text)) + + try: + date = _get_iso_8601_week( + m.group("isoyear"), m.group("isoweek"), m.group("isoweekday") + ) + except ParserError: + raise + except ValueError: + raise ParserError("Invalid date string: {}".format(text)) + + year = date["year"] + month = date["month"] + day = date["day"] + else: + # We have a classic date representation + year = int(m.group("year")) + + if not m.group("monthday"): + # No month and day + month = 1 + day = 1 + else: + if m.group("month") and m.group("day"): + # Month and day + if not m.group("daysep") and len(m.group("day")) == 1: + # Ordinal day + ordinal = int(m.group("month") + m.group("day")) + leap = is_leap(year) + months_offsets = MONTHS_OFFSETS[leap] + + if ordinal > months_offsets[13]: + raise ParserError("Ordinal day is out of range") + + for i in range(1, 14): + if ordinal <= months_offsets[i]: + day = ordinal - months_offsets[i - 1] + month = i - 1 + + break + else: + month = int(m.group("month")) + day = int(m.group("day")) + else: + # Only month + if not m.group("monthsep"): + # The date looks like 201207 + # which is invalid for a date + # But it might be a time in the form hhmmss + ambiguous_date = True + + month = int(m.group("month")) + day = 1 + + if not m.group("time"): + # No time has been specified + if ambiguous_date: + # We can "safely" assume that the ambiguous date + # was actually a time in the form hhmmss + hhmmss = "{}{:0>2}".format(str(year), str(month)) + + return datetime.time(int(hhmmss[:2]), int(hhmmss[2:4]), int(hhmmss[4:])) + + return datetime.date(year, month, day) + + if ambiguous_date: + raise ParserError("Invalid date string: {}".format(text)) + + if is_date and not m.group("timesep"): + raise ParserError("Invalid date string: {}".format(text)) + + if not is_date: + is_time = True + + # Grabbing hh:mm:ss + hour = int(m.group("hour")) + minsep = m.group("minsep") + + if m.group("minute"): + minute = int(m.group("minute")) + elif minsep: + raise ParserError("Invalid ISO 8601 time part") + + secsep = m.group("secsep") + if secsep and not minsep and m.group("minute"): + # minute/second separator but no hour/minute separator + raise ParserError("Invalid ISO 8601 time part") + + if m.group("second"): + if not secsep and minsep: + # No minute/second separator but hour/minute separator + raise ParserError("Invalid ISO 8601 time part") + + second = int(m.group("second")) + elif secsep: + raise ParserError("Invalid ISO 8601 time part") + + # Grabbing subseconds, if any + if m.group("subsecondsection"): + # Limiting to 6 chars + subsecond = m.group("subsecond")[:6] + + microsecond = int("{:0<6}".format(subsecond)) + + # Grabbing timezone, if any + tz = m.group("tz") + if tz: + if tz == "Z": + tzinfo = UTC + else: + negative = True if tz.startswith("-") else False + tz = tz[1:] + if ":" not in tz: + if len(tz) == 2: + tz = "{}00".format(tz) + + off_hour = tz[0:2] + off_minute = tz[2:4] + else: + off_hour, off_minute = tz.split(":") + + offset = ((int(off_hour) * 60) + int(off_minute)) * 60 + + if negative: + offset = -1 * offset + + tzinfo = FixedTimezone(offset) + + if is_time: + return datetime.time(hour, minute, second, microsecond) + + return datetime.datetime( + year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo + ) + + +def _parse_iso8601_duration(text, **options): + m = ISO8601_DURATION.match(text) + if not m: + return + + years = 0 + months = 0 + weeks = 0 + days = 0 + hours = 0 + minutes = 0 + seconds = 0 + microseconds = 0 + fractional = False + + if m.group("w"): + # Weeks + if m.group("ymd") or m.group("hms"): + # Specifying anything more than weeks is not supported + raise ParserError("Invalid duration string") + + _weeks = m.group("weeks") + if not _weeks: + raise ParserError("Invalid duration string") + + _weeks = _weeks.replace(",", ".").replace("W", "") + if "." in _weeks: + _weeks, portion = _weeks.split(".") + weeks = int(_weeks) + _days = int(portion) / 10 * 7 + days, hours = int(_days // 1), _days % 1 * HOURS_PER_DAY + else: + weeks = int(_weeks) + + if m.group("ymd"): + # Years, months and/or days + _years = m.group("years") + _months = m.group("months") + _days = m.group("days") + + # Checking order + years_start = m.start("years") if _years else -3 + months_start = m.start("months") if _months else years_start + 1 + days_start = m.start("days") if _days else months_start + 1 + + # Check correct order + if not (years_start < months_start < days_start): + raise ParserError("Invalid duration") + + if _years: + _years = _years.replace(",", ".").replace("Y", "") + if "." in _years: + raise ParserError("Float years in duration are not supported") + else: + years = int(_years) + + if _months: + if fractional: + raise ParserError("Invalid duration") + + _months = _months.replace(",", ".").replace("M", "") + if "." in _months: + raise ParserError("Float months in duration are not supported") + else: + months = int(_months) + + if _days: + if fractional: + raise ParserError("Invalid duration") + + _days = _days.replace(",", ".").replace("D", "") + + if "." in _days: + fractional = True + + _days, _hours = _days.split(".") + days = int(_days) + hours = int(_hours) / 10 * HOURS_PER_DAY + else: + days = int(_days) + + if m.group("hms"): + # Hours, minutes and/or seconds + _hours = m.group("hours") or 0 + _minutes = m.group("minutes") or 0 + _seconds = m.group("seconds") or 0 + + # Checking order + hours_start = m.start("hours") if _hours else -3 + minutes_start = m.start("minutes") if _minutes else hours_start + 1 + seconds_start = m.start("seconds") if _seconds else minutes_start + 1 + + # Check correct order + if not (hours_start < minutes_start < seconds_start): + raise ParserError("Invalid duration") + + if _hours: + if fractional: + raise ParserError("Invalid duration") + + _hours = _hours.replace(",", ".").replace("H", "") + + if "." in _hours: + fractional = True + + _hours, _mins = _hours.split(".") + hours += int(_hours) + minutes += int(_mins) / 10 * MINUTES_PER_HOUR + else: + hours += int(_hours) + + if _minutes: + if fractional: + raise ParserError("Invalid duration") + + _minutes = _minutes.replace(",", ".").replace("M", "") + + if "." in _minutes: + fractional = True + + _minutes, _secs = _minutes.split(".") + minutes += int(_minutes) + seconds += int(_secs) / 10 * SECONDS_PER_MINUTE + else: + minutes += int(_minutes) + + if _seconds: + if fractional: + raise ParserError("Invalid duration") + + _seconds = _seconds.replace(",", ".").replace("S", "") + + if "." in _seconds: + _seconds, _microseconds = _seconds.split(".") + seconds += int(_seconds) + microseconds += int("{:0<6}".format(_microseconds[:6])) + else: + seconds += int(_seconds) + + return Duration( + years=years, + months=months, + weeks=weeks, + days=days, + hours=hours, + minutes=minutes, + seconds=seconds, + microseconds=microseconds, + ) + + +def _get_iso_8601_week(year, week, weekday): + if not weekday: + weekday = 1 + else: + weekday = int(weekday) + + year = int(year) + week = int(week) + + if week > 53 or week > 52 and not is_long_year(year): + raise ParserError("Invalid week for week date") + + if weekday > 7: + raise ParserError("Invalid weekday for week date") + + # We can't rely on strptime directly here since + # it does not support ISO week date + ordinal = week * 7 + weekday - (week_day(year, 1, 4) + 3) + + if ordinal < 1: + # Previous year + ordinal += days_in_year(year - 1) + year -= 1 + + if ordinal > days_in_year(year): + # Next year + ordinal -= days_in_year(year) + year += 1 + + fmt = "%Y-%j" + string = "{}-{}".format(year, ordinal) + + dt = datetime.datetime.strptime(string, fmt) + + return {"year": dt.year, "month": dt.month, "day": dt.day} diff --git a/pendulum/period.py b/pendulum/period.py new file mode 100644 index 0000000..c66c6b9 --- /dev/null +++ b/pendulum/period.py @@ -0,0 +1,390 @@ +from __future__ import absolute_import + +import operator + +from datetime import date +from datetime import datetime +from datetime import timedelta + +import pendulum + +from pendulum.utils._compat import _HAS_FOLD +from pendulum.utils._compat import decode + +from .constants import MONTHS_PER_YEAR +from .duration import Duration +from .helpers import precise_diff + + +class Period(Duration): + """ + Duration class that is aware of the datetimes that generated the + time difference. + """ + + def __new__(cls, start, end, absolute=False): + if isinstance(start, datetime) and isinstance(end, datetime): + if ( + start.tzinfo is None + and end.tzinfo is not None + or start.tzinfo is not None + and end.tzinfo is None + ): + raise TypeError("can't compare offset-naive and offset-aware datetimes") + + if absolute and start > end: + end, start = start, end + + _start = start + _end = end + if isinstance(start, pendulum.DateTime): + if _HAS_FOLD: + _start = datetime( + start.year, + start.month, + start.day, + start.hour, + start.minute, + start.second, + start.microsecond, + tzinfo=start.tzinfo, + fold=start.fold, + ) + else: + _start = datetime( + start.year, + start.month, + start.day, + start.hour, + start.minute, + start.second, + start.microsecond, + tzinfo=start.tzinfo, + ) + elif isinstance(start, pendulum.Date): + _start = date(start.year, start.month, start.day) + + if isinstance(end, pendulum.DateTime): + if _HAS_FOLD: + _end = datetime( + end.year, + end.month, + end.day, + end.hour, + end.minute, + end.second, + end.microsecond, + tzinfo=end.tzinfo, + fold=end.fold, + ) + else: + _end = datetime( + end.year, + end.month, + end.day, + end.hour, + end.minute, + end.second, + end.microsecond, + tzinfo=end.tzinfo, + ) + elif isinstance(end, pendulum.Date): + _end = date(end.year, end.month, end.day) + + # Fixing issues with datetime.__sub__() + # not handling offsets if the tzinfo is the same + if ( + isinstance(_start, datetime) + and isinstance(_end, datetime) + and _start.tzinfo is _end.tzinfo + ): + if _start.tzinfo is not None: + _start = (_start - start.utcoffset()).replace(tzinfo=None) + + if isinstance(end, datetime) and _end.tzinfo is not None: + _end = (_end - end.utcoffset()).replace(tzinfo=None) + + delta = _end - _start + + return super(Period, cls).__new__(cls, seconds=delta.total_seconds()) + + def __init__(self, start, end, absolute=False): + super(Period, self).__init__() + + if not isinstance(start, pendulum.Date): + if isinstance(start, datetime): + start = pendulum.instance(start) + else: + start = pendulum.date(start.year, start.month, start.day) + + _start = start + else: + if isinstance(start, pendulum.DateTime): + _start = datetime( + start.year, + start.month, + start.day, + start.hour, + start.minute, + start.second, + start.microsecond, + tzinfo=start.tzinfo, + ) + else: + _start = date(start.year, start.month, start.day) + + if not isinstance(end, pendulum.Date): + if isinstance(end, datetime): + end = pendulum.instance(end) + else: + end = pendulum.date(end.year, end.month, end.day) + + _end = end + else: + if isinstance(end, pendulum.DateTime): + _end = datetime( + end.year, + end.month, + end.day, + end.hour, + end.minute, + end.second, + end.microsecond, + tzinfo=end.tzinfo, + ) + else: + _end = date(end.year, end.month, end.day) + + self._invert = False + if start > end: + self._invert = True + + if absolute: + end, start = start, end + _end, _start = _start, _end + + self._absolute = absolute + self._start = start + self._end = end + self._delta = precise_diff(_start, _end) + + @property + def years(self): + return self._delta.years + + @property + def months(self): + return self._delta.months + + @property + def weeks(self): + return abs(self._delta.days) // 7 * self._sign(self._delta.days) + + @property + def days(self): + return self._days + + @property + def remaining_days(self): + return abs(self._delta.days) % 7 * self._sign(self._days) + + @property + def hours(self): + return self._delta.hours + + @property + def minutes(self): + return self._delta.minutes + + @property + def start(self): + return self._start + + @property + def end(self): + return self._end + + def in_years(self): + """ + Gives the duration of the Period in full years. + + :rtype: int + """ + return self.years + + def in_months(self): + """ + Gives the duration of the Period in full months. + + :rtype: int + """ + return self.years * MONTHS_PER_YEAR + self.months + + def in_weeks(self): + days = self.in_days() + sign = 1 + + if days < 0: + sign = -1 + + return sign * (abs(days) // 7) + + def in_days(self): + return self._delta.total_days + + def in_words(self, locale=None, separator=" "): + """ + Get the current interval in words in the current locale. + + Ex: 6 jours 23 heures 58 minutes + + :param locale: The locale to use. Defaults to current locale. + :type locale: str + + :param separator: The separator to use between each unit + :type separator: str + + :rtype: str + """ + periods = [ + ("year", self.years), + ("month", self.months), + ("week", self.weeks), + ("day", self.remaining_days), + ("hour", self.hours), + ("minute", self.minutes), + ("second", self.remaining_seconds), + ] + + if locale is None: + locale = pendulum.get_locale() + + locale = pendulum.locale(locale) + parts = [] + for period in periods: + unit, count = period + if abs(count) > 0: + translation = locale.translation( + "units.{}.{}".format(unit, locale.plural(abs(count))) + ) + parts.append(translation.format(count)) + + if not parts: + if abs(self.microseconds) > 0: + unit = "units.second.{}".format(locale.plural(1)) + count = "{:.2f}".format(abs(self.microseconds) / 1e6) + else: + unit = "units.microsecond.{}".format(locale.plural(0)) + count = 0 + translation = locale.translation(unit) + parts.append(translation.format(count)) + + return decode(separator.join(parts)) + + def range(self, unit, amount=1): + method = "add" + op = operator.le + if not self._absolute and self.invert: + method = "subtract" + op = operator.ge + + start, end = self.start, self.end + + i = amount + while op(start, end): + yield start + + start = getattr(self.start, method)(**{unit: i}) + + i += amount + + def as_interval(self): + """ + Return the Period as an Duration. + + :rtype: Duration + """ + return Duration(seconds=self.total_seconds()) + + def __iter__(self): + return self.range("days") + + def __contains__(self, item): + return self.start <= item <= self.end + + def __add__(self, other): + return self.as_interval().__add__(other) + + __radd__ = __add__ + + def __sub__(self, other): + return self.as_interval().__sub__(other) + + def __neg__(self): + return self.__class__(self.end, self.start, self._absolute) + + def __mul__(self, other): + return self.as_interval().__mul__(other) + + __rmul__ = __mul__ + + def __floordiv__(self, other): + return self.as_interval().__floordiv__(other) + + def __truediv__(self, other): + return self.as_interval().__truediv__(other) + + __div__ = __floordiv__ + + def __mod__(self, other): + return self.as_interval().__mod__(other) + + def __divmod__(self, other): + return self.as_interval().__divmod__(other) + + def __abs__(self): + return self.__class__(self.start, self.end, True) + + def __repr__(self): + return "<Period [{} -> {}]>".format(self._start, self._end) + + def __str__(self): + return self.__repr__() + + def _cmp(self, other): + # Only needed for PyPy + assert isinstance(other, timedelta) + + if isinstance(other, Period): + other = other.as_timedelta() + + td = self.as_timedelta() + + return 0 if td == other else 1 if td > other else -1 + + def _getstate(self, protocol=3): + start, end = self.start, self.end + + if self._invert and self._absolute: + end, start = start, end + + return (start, end, self._absolute) + + def __reduce__(self): + return self.__reduce_ex__(2) + + def __reduce_ex__(self, protocol): + return self.__class__, self._getstate(protocol) + + def __hash__(self): + return hash((self.start, self.end, self._absolute)) + + def __eq__(self, other): + if isinstance(other, Period): + return (self.start, self.end, self._absolute) == ( + other.start, + other.end, + other._absolute, + ) + else: + return self.as_interval() == other diff --git a/pendulum/py.typed b/pendulum/py.typed new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/py.typed diff --git a/pendulum/time.py b/pendulum/time.py new file mode 100644 index 0000000..6dea4a0 --- /dev/null +++ b/pendulum/time.py @@ -0,0 +1,284 @@ +from __future__ import absolute_import + +from datetime import time +from datetime import timedelta + +import pendulum + +from .constants import SECS_PER_HOUR +from .constants import SECS_PER_MIN +from .constants import USECS_PER_SEC +from .duration import AbsoluteDuration +from .duration import Duration +from .mixins.default import FormattableMixin + + +class Time(FormattableMixin, time): + """ + Represents a time instance as hour, minute, second, microsecond. + """ + + # String formatting + def __repr__(self): + us = "" + if self.microsecond: + us = ", {}".format(self.microsecond) + + tzinfo = "" + if self.tzinfo: + tzinfo = ", tzinfo={}".format(repr(self.tzinfo)) + + return "{}({}, {}, {}{}{})".format( + self.__class__.__name__, self.hour, self.minute, self.second, us, tzinfo + ) + + # Comparisons + + def closest(self, dt1, dt2): + """ + Get the closest time from the instance. + + :type dt1: Time or time + :type dt2: Time or time + + :rtype: Time + """ + dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond) + dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond) + + if self.diff(dt1).in_seconds() < self.diff(dt2).in_seconds(): + return dt1 + + return dt2 + + def farthest(self, dt1, dt2): + """ + Get the farthest time from the instance. + + :type dt1: Time or time + :type dt2: Time or time + + :rtype: Time + """ + dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond) + dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond) + + if self.diff(dt1).in_seconds() > self.diff(dt2).in_seconds(): + return dt1 + + return dt2 + + # ADDITIONS AND SUBSTRACTIONS + + def add(self, hours=0, minutes=0, seconds=0, microseconds=0): + """ + Add duration to the instance. + + :param hours: The number of hours + :type hours: int + + :param minutes: The number of minutes + :type minutes: int + + :param seconds: The number of seconds + :type seconds: int + + :param microseconds: The number of microseconds + :type microseconds: int + + :rtype: Time + """ + from .datetime import DateTime + + return ( + DateTime.EPOCH.at(self.hour, self.minute, self.second, self.microsecond) + .add( + hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds + ) + .time() + ) + + def subtract(self, hours=0, minutes=0, seconds=0, microseconds=0): + """ + Add duration to the instance. + + :param hours: The number of hours + :type hours: int + + :param minutes: The number of minutes + :type minutes: int + + :param seconds: The number of seconds + :type seconds: int + + :param microseconds: The number of microseconds + :type microseconds: int + + :rtype: Time + """ + from .datetime import DateTime + + return ( + DateTime.EPOCH.at(self.hour, self.minute, self.second, self.microsecond) + .subtract( + hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds + ) + .time() + ) + + def add_timedelta(self, delta): + """ + Add timedelta duration to the instance. + + :param delta: The timedelta instance + :type delta: datetime.timedelta + + :rtype: Time + """ + if delta.days: + raise TypeError("Cannot add timedelta with days to Time.") + + return self.add(seconds=delta.seconds, microseconds=delta.microseconds) + + def subtract_timedelta(self, delta): + """ + Remove timedelta duration from the instance. + + :param delta: The timedelta instance + :type delta: datetime.timedelta + + :rtype: Time + """ + if delta.days: + raise TypeError("Cannot subtract timedelta with days to Time.") + + return self.subtract(seconds=delta.seconds, microseconds=delta.microseconds) + + def __add__(self, other): + if not isinstance(other, timedelta): + return NotImplemented + + return self.add_timedelta(other) + + def __sub__(self, other): + if not isinstance(other, (Time, time, timedelta)): + return NotImplemented + + if isinstance(other, timedelta): + return self.subtract_timedelta(other) + + if isinstance(other, time): + if other.tzinfo is not None: + raise TypeError("Cannot subtract aware times to or from Time.") + + other = self.__class__( + other.hour, other.minute, other.second, other.microsecond + ) + + return other.diff(self, False) + + def __rsub__(self, other): + if not isinstance(other, (Time, time)): + return NotImplemented + + if isinstance(other, time): + if other.tzinfo is not None: + raise TypeError("Cannot subtract aware times to or from Time.") + + other = self.__class__( + other.hour, other.minute, other.second, other.microsecond + ) + + return other.__sub__(self) + + # DIFFERENCES + + def diff(self, dt=None, abs=True): + """ + Returns the difference between two Time objects as an Duration. + + :type dt: Time or None + + :param abs: Whether to return an absolute interval or not + :type abs: bool + + :rtype: Duration + """ + if dt is None: + dt = pendulum.now().time() + else: + dt = self.__class__(dt.hour, dt.minute, dt.second, dt.microsecond) + + us1 = ( + self.hour * SECS_PER_HOUR + self.minute * SECS_PER_MIN + self.second + ) * USECS_PER_SEC + + us2 = ( + dt.hour * SECS_PER_HOUR + dt.minute * SECS_PER_MIN + dt.second + ) * USECS_PER_SEC + + klass = Duration + if abs: + klass = AbsoluteDuration + + return klass(microseconds=us2 - us1) + + def diff_for_humans(self, other=None, absolute=False, locale=None): + """ + Get the difference in a human readable format in the current locale. + + :type other: Time or time + + :param absolute: removes time difference modifiers ago, after, etc + :type absolute: bool + + :param locale: The locale to use for localization + :type locale: str + + :rtype: str + """ + is_now = other is None + + if is_now: + other = pendulum.now().time() + + diff = self.diff(other) + + return pendulum.format_diff(diff, is_now, absolute, locale) + + # Compatibility methods + + def replace( + self, hour=None, minute=None, second=None, microsecond=None, tzinfo=True + ): + if tzinfo is True: + tzinfo = self.tzinfo + + hour = hour if hour is not None else self.hour + minute = minute if minute is not None else self.minute + second = second if second is not None else self.second + microsecond = microsecond if microsecond is not None else self.microsecond + + t = super(Time, self).replace(hour, minute, second, microsecond, tzinfo=tzinfo) + return self.__class__( + t.hour, t.minute, t.second, t.microsecond, tzinfo=t.tzinfo + ) + + def __getnewargs__(self): + return (self,) + + def _get_state(self, protocol=3): + tz = self.tzinfo + + return (self.hour, self.minute, self.second, self.microsecond, tz) + + def __reduce__(self): + return self.__reduce_ex__(2) + + def __reduce_ex__(self, protocol): + return self.__class__, self._get_state(protocol) + + +Time.min = Time(0, 0, 0) +Time.max = Time(23, 59, 59, 999999) +Time.resolution = Duration(microseconds=1) diff --git a/pendulum/tz/__init__.py b/pendulum/tz/__init__.py new file mode 100644 index 0000000..e45a6cd --- /dev/null +++ b/pendulum/tz/__init__.py @@ -0,0 +1,60 @@ +from typing import Tuple +from typing import Union + +import pytzdata + +from .local_timezone import get_local_timezone +from .local_timezone import set_local_timezone +from .local_timezone import test_local_timezone +from .timezone import UTC +from .timezone import FixedTimezone as _FixedTimezone +from .timezone import Timezone as _Timezone + + +PRE_TRANSITION = "pre" +POST_TRANSITION = "post" +TRANSITION_ERROR = "error" + +timezones = pytzdata.timezones # type: Tuple[str, ...] + + +_tz_cache = {} + + +def timezone(name, extended=True): # type: (Union[str, int], bool) -> _Timezone + """ + Return a Timezone instance given its name. + """ + if isinstance(name, int): + return fixed_timezone(name) + + if name.lower() == "utc": + return UTC + + if name in _tz_cache: + return _tz_cache[name] + + tz = _Timezone(name, extended=extended) + _tz_cache[name] = tz + + return tz + + +def fixed_timezone(offset): # type: (int) -> _FixedTimezone + """ + Return a Timezone instance given its offset in seconds. + """ + if offset in _tz_cache: + return _tz_cache[offset] # type: ignore + + tz = _FixedTimezone(offset) + _tz_cache[offset] = tz + + return tz + + +def local_timezone(): # type: () -> _Timezone + """ + Return the local timezone. + """ + return get_local_timezone() diff --git a/pendulum/tz/data/__init__.py b/pendulum/tz/data/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/tz/data/__init__.py diff --git a/pendulum/tz/data/windows.py b/pendulum/tz/data/windows.py new file mode 100644 index 0000000..68bcfeb --- /dev/null +++ b/pendulum/tz/data/windows.py @@ -0,0 +1,137 @@ +windows_timezones = { + "AUS Central Standard Time": "Australia/Darwin", + "AUS Eastern Standard Time": "Australia/Sydney", + "Afghanistan Standard Time": "Asia/Kabul", + "Alaskan Standard Time": "America/Anchorage", + "Aleutian Standard Time": "America/Adak", + "Altai Standard Time": "Asia/Barnaul", + "Arab Standard Time": "Asia/Riyadh", + "Arabian Standard Time": "Asia/Dubai", + "Arabic Standard Time": "Asia/Baghdad", + "Argentina Standard Time": "America/Buenos_Aires", + "Astrakhan Standard Time": "Europe/Astrakhan", + "Atlantic Standard Time": "America/Halifax", + "Aus Central W. Standard Time": "Australia/Eucla", + "Azerbaijan Standard Time": "Asia/Baku", + "Azores Standard Time": "Atlantic/Azores", + "Bahia Standard Time": "America/Bahia", + "Bangladesh Standard Time": "Asia/Dhaka", + "Belarus Standard Time": "Europe/Minsk", + "Bougainville Standard Time": "Pacific/Bougainville", + "Canada Central Standard Time": "America/Regina", + "Cape Verde Standard Time": "Atlantic/Cape_Verde", + "Caucasus Standard Time": "Asia/Yerevan", + "Cen. Australia Standard Time": "Australia/Adelaide", + "Central America Standard Time": "America/Guatemala", + "Central Asia Standard Time": "Asia/Almaty", + "Central Brazilian Standard Time": "America/Cuiaba", + "Central Europe Standard Time": "Europe/Budapest", + "Central European Standard Time": "Europe/Warsaw", + "Central Pacific Standard Time": "Pacific/Guadalcanal", + "Central Standard Time": "America/Chicago", + "Central Standard Time (Mexico)": "America/Mexico_City", + "Chatham Islands Standard Time": "Pacific/Chatham", + "China Standard Time": "Asia/Shanghai", + "Cuba Standard Time": "America/Havana", + "Dateline Standard Time": "Etc/GMT+12", + "E. Africa Standard Time": "Africa/Nairobi", + "E. Australia Standard Time": "Australia/Brisbane", + "E. Europe Standard Time": "Europe/Chisinau", + "E. South America Standard Time": "America/Sao_Paulo", + "Easter Island Standard Time": "Pacific/Easter", + "Eastern Standard Time": "America/New_York", + "Eastern Standard Time (Mexico)": "America/Cancun", + "Egypt Standard Time": "Africa/Cairo", + "Ekaterinburg Standard Time": "Asia/Yekaterinburg", + "FLE Standard Time": "Europe/Kiev", + "Fiji Standard Time": "Pacific/Fiji", + "GMT Standard Time": "Europe/London", + "GTB Standard Time": "Europe/Bucharest", + "Georgian Standard Time": "Asia/Tbilisi", + "Greenland Standard Time": "America/Godthab", + "Greenwich Standard Time": "Atlantic/Reykjavik", + "Haiti Standard Time": "America/Port-au-Prince", + "Hawaiian Standard Time": "Pacific/Honolulu", + "India Standard Time": "Asia/Calcutta", + "Iran Standard Time": "Asia/Tehran", + "Israel Standard Time": "Asia/Jerusalem", + "Jordan Standard Time": "Asia/Amman", + "Kaliningrad Standard Time": "Europe/Kaliningrad", + "Korea Standard Time": "Asia/Seoul", + "Libya Standard Time": "Africa/Tripoli", + "Line Islands Standard Time": "Pacific/Kiritimati", + "Lord Howe Standard Time": "Australia/Lord_Howe", + "Magadan Standard Time": "Asia/Magadan", + "Magallanes Standard Time": "America/Punta_Arenas", + "Marquesas Standard Time": "Pacific/Marquesas", + "Mauritius Standard Time": "Indian/Mauritius", + "Middle East Standard Time": "Asia/Beirut", + "Montevideo Standard Time": "America/Montevideo", + "Morocco Standard Time": "Africa/Casablanca", + "Mountain Standard Time": "America/Denver", + "Mountain Standard Time (Mexico)": "America/Chihuahua", + "Myanmar Standard Time": "Asia/Rangoon", + "N. Central Asia Standard Time": "Asia/Novosibirsk", + "Namibia Standard Time": "Africa/Windhoek", + "Nepal Standard Time": "Asia/Katmandu", + "New Zealand Standard Time": "Pacific/Auckland", + "Newfoundland Standard Time": "America/St_Johns", + "Norfolk Standard Time": "Pacific/Norfolk", + "North Asia East Standard Time": "Asia/Irkutsk", + "North Asia Standard Time": "Asia/Krasnoyarsk", + "North Korea Standard Time": "Asia/Pyongyang", + "Omsk Standard Time": "Asia/Omsk", + "Pacific SA Standard Time": "America/Santiago", + "Pacific Standard Time": "America/Los_Angeles", + "Pacific Standard Time (Mexico)": "America/Tijuana", + "Pakistan Standard Time": "Asia/Karachi", + "Paraguay Standard Time": "America/Asuncion", + "Romance Standard Time": "Europe/Paris", + "Russia Time Zone 10": "Asia/Srednekolymsk", + "Russia Time Zone 11": "Asia/Kamchatka", + "Russia Time Zone 3": "Europe/Samara", + "Russian Standard Time": "Europe/Moscow", + "SA Eastern Standard Time": "America/Cayenne", + "SA Pacific Standard Time": "America/Bogota", + "SA Western Standard Time": "America/La_Paz", + "SE Asia Standard Time": "Asia/Bangkok", + "Saint Pierre Standard Time": "America/Miquelon", + "Sakhalin Standard Time": "Asia/Sakhalin", + "Samoa Standard Time": "Pacific/Apia", + "Sao Tome Standard Time": "Africa/Sao_Tome", + "Saratov Standard Time": "Europe/Saratov", + "Singapore Standard Time": "Asia/Singapore", + "South Africa Standard Time": "Africa/Johannesburg", + "Sri Lanka Standard Time": "Asia/Colombo", + "Sudan Standard Time": "Africa/Khartoum", + "Syria Standard Time": "Asia/Damascus", + "Taipei Standard Time": "Asia/Taipei", + "Tasmania Standard Time": "Australia/Hobart", + "Tocantins Standard Time": "America/Araguaina", + "Tokyo Standard Time": "Asia/Tokyo", + "Tomsk Standard Time": "Asia/Tomsk", + "Tonga Standard Time": "Pacific/Tongatapu", + "Transbaikal Standard Time": "Asia/Chita", + "Turkey Standard Time": "Europe/Istanbul", + "Turks And Caicos Standard Time": "America/Grand_Turk", + "US Eastern Standard Time": "America/Indianapolis", + "US Mountain Standard Time": "America/Phoenix", + "UTC": "Etc/GMT", + "UTC+12": "Etc/GMT-12", + "UTC+13": "Etc/GMT-13", + "UTC-02": "Etc/GMT+2", + "UTC-08": "Etc/GMT+8", + "UTC-09": "Etc/GMT+9", + "UTC-11": "Etc/GMT+11", + "Ulaanbaatar Standard Time": "Asia/Ulaanbaatar", + "Venezuela Standard Time": "America/Caracas", + "Vladivostok Standard Time": "Asia/Vladivostok", + "W. Australia Standard Time": "Australia/Perth", + "W. Central Africa Standard Time": "Africa/Lagos", + "W. Europe Standard Time": "Europe/Berlin", + "W. Mongolia Standard Time": "Asia/Hovd", + "West Asia Standard Time": "Asia/Tashkent", + "West Bank Standard Time": "Asia/Hebron", + "West Pacific Standard Time": "Pacific/Port_Moresby", + "Yakutsk Standard Time": "Asia/Yakutsk", +} diff --git a/pendulum/tz/exceptions.py b/pendulum/tz/exceptions.py new file mode 100644 index 0000000..b91fa06 --- /dev/null +++ b/pendulum/tz/exceptions.py @@ -0,0 +1,23 @@ +class TimezoneError(ValueError): + + pass + + +class NonExistingTime(TimezoneError): + + message = "The datetime {} does not exist." + + def __init__(self, dt): + message = self.message.format(dt) + + super(NonExistingTime, self).__init__(message) + + +class AmbiguousTime(TimezoneError): + + message = "The datetime {} is ambiguous." + + def __init__(self, dt): + message = self.message.format(dt) + + super(AmbiguousTime, self).__init__(message) diff --git a/pendulum/tz/local_timezone.py b/pendulum/tz/local_timezone.py new file mode 100644 index 0000000..08a6e4f --- /dev/null +++ b/pendulum/tz/local_timezone.py @@ -0,0 +1,257 @@ +import os +import re +import sys + +from contextlib import contextmanager +from typing import Iterator +from typing import Optional +from typing import Union + +from .timezone import Timezone +from .timezone import TimezoneFile +from .zoneinfo.exceptions import InvalidTimezone + + +try: + import _winreg as winreg +except ImportError: + try: + import winreg + except ImportError: + winreg = None + + +_mock_local_timezone = None +_local_timezone = None + + +def get_local_timezone(): # type: () -> Timezone + global _local_timezone + + if _mock_local_timezone is not None: + return _mock_local_timezone + + if _local_timezone is None: + tz = _get_system_timezone() + + _local_timezone = tz + + return _local_timezone + + +def set_local_timezone(mock=None): # type: (Optional[Union[str, Timezone]]) -> None + global _mock_local_timezone + + _mock_local_timezone = mock + + +@contextmanager +def test_local_timezone(mock): # type: (Timezone) -> Iterator[None] + set_local_timezone(mock) + + yield + + set_local_timezone() + + +def _get_system_timezone(): # type: () -> Timezone + if sys.platform == "win32": + return _get_windows_timezone() + elif "darwin" in sys.platform: + return _get_darwin_timezone() + + return _get_unix_timezone() + + +def _get_windows_timezone(): # type: () -> Timezone + from .data.windows import windows_timezones + + # Windows is special. It has unique time zone names (in several + # meanings of the word) available, but unfortunately, they can be + # translated to the language of the operating system, so we need to + # do a backwards lookup, by going through all time zones and see which + # one matches. + handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) + + tz_local_key_name = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" + localtz = winreg.OpenKey(handle, tz_local_key_name) + + timezone_info = {} + size = winreg.QueryInfoKey(localtz)[1] + for i in range(size): + data = winreg.EnumValue(localtz, i) + timezone_info[data[0]] = data[1] + + localtz.Close() + + if "TimeZoneKeyName" in timezone_info: + # Windows 7 (and Vista?) + + # For some reason this returns a string with loads of NUL bytes at + # least on some systems. I don't know if this is a bug somewhere, I + # just work around it. + tzkeyname = timezone_info["TimeZoneKeyName"].split("\x00", 1)[0] + else: + # Windows 2000 or XP + + # This is the localized name: + tzwin = timezone_info["StandardName"] + + # Open the list of timezones to look up the real name: + tz_key_name = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" + tzkey = winreg.OpenKey(handle, tz_key_name) + + # Now, match this value to Time Zone information + tzkeyname = None + for i in range(winreg.QueryInfoKey(tzkey)[0]): + subkey = winreg.EnumKey(tzkey, i) + sub = winreg.OpenKey(tzkey, subkey) + + info = {} + size = winreg.QueryInfoKey(sub)[1] + for i in range(size): + data = winreg.EnumValue(sub, i) + info[data[0]] = data[1] + + sub.Close() + try: + if info["Std"] == tzwin: + tzkeyname = subkey + break + except KeyError: + # This timezone didn't have proper configuration. + # Ignore it. + pass + + tzkey.Close() + handle.Close() + + if tzkeyname is None: + raise LookupError("Can not find Windows timezone configuration") + + timezone = windows_timezones.get(tzkeyname) + if timezone is None: + # Nope, that didn't work. Try adding "Standard Time", + # it seems to work a lot of times: + timezone = windows_timezones.get(tzkeyname + " Standard Time") + + # Return what we have. + if timezone is None: + raise LookupError("Unable to find timezone " + tzkeyname) + + return Timezone(timezone) + + +def _get_darwin_timezone(): # type: () -> Timezone + # link will be something like /usr/share/zoneinfo/America/Los_Angeles. + link = os.readlink("/etc/localtime") + tzname = link[link.rfind("zoneinfo/") + 9 :] + + return Timezone(tzname) + + +def _get_unix_timezone(_root="/"): # type: (str) -> Timezone + tzenv = os.environ.get("TZ") + if tzenv: + try: + return _tz_from_env(tzenv) + except ValueError: + pass + + # Now look for distribution specific configuration files + # that contain the timezone name. + tzpath = os.path.join(_root, "etc/timezone") + if os.path.exists(tzpath): + with open(tzpath, "rb") as tzfile: + data = tzfile.read() + + # Issue #3 was that /etc/timezone was a zoneinfo file. + # That's a misconfiguration, but we need to handle it gracefully: + if data[:5] != "TZif2": + etctz = data.strip().decode() + # Get rid of host definitions and comments: + if " " in etctz: + etctz, dummy = etctz.split(" ", 1) + if "#" in etctz: + etctz, dummy = etctz.split("#", 1) + + return Timezone(etctz.replace(" ", "_")) + + # CentOS has a ZONE setting in /etc/sysconfig/clock, + # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and + # Gentoo has a TIMEZONE setting in /etc/conf.d/clock + # We look through these files for a timezone: + zone_re = re.compile(r'\s*ZONE\s*=\s*"') + timezone_re = re.compile(r'\s*TIMEZONE\s*=\s*"') + end_re = re.compile('"') + + for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"): + tzpath = os.path.join(_root, filename) + if not os.path.exists(tzpath): + continue + + with open(tzpath, "rt") as tzfile: + data = tzfile.readlines() + + for line in data: + # Look for the ZONE= setting. + match = zone_re.match(line) + if match is None: + # No ZONE= setting. Look for the TIMEZONE= setting. + match = timezone_re.match(line) + + if match is not None: + # Some setting existed + line = line[match.end() :] + etctz = line[: end_re.search(line).start()] + + parts = list(reversed(etctz.replace(" ", "_").split(os.path.sep))) + tzpath = [] + while parts: + tzpath.insert(0, parts.pop(0)) + + try: + return Timezone(os.path.join(*tzpath)) + except InvalidTimezone: + pass + + # systemd distributions use symlinks that include the zone name, + # see manpage of localtime(5) and timedatectl(1) + tzpath = os.path.join(_root, "etc", "localtime") + if os.path.exists(tzpath) and os.path.islink(tzpath): + parts = list( + reversed(os.path.realpath(tzpath).replace(" ", "_").split(os.path.sep)) + ) + tzpath = [] + while parts: + tzpath.insert(0, parts.pop(0)) + try: + return Timezone(os.path.join(*tzpath)) + except InvalidTimezone: + pass + + # No explicit setting existed. Use localtime + for filename in ("etc/localtime", "usr/local/etc/localtime"): + tzpath = os.path.join(_root, filename) + + if not os.path.exists(tzpath): + continue + + return TimezoneFile(tzpath) + + raise RuntimeError("Unable to find any timezone configuration") + + +def _tz_from_env(tzenv): # type: (str) -> Timezone + if tzenv[0] == ":": + tzenv = tzenv[1:] + + # TZ specifies a file + if os.path.exists(tzenv): + return TimezoneFile(tzenv) + + # TZ specifies a zoneinfo zone. + try: + return Timezone(tzenv) + except ValueError: + raise diff --git a/pendulum/tz/timezone.py b/pendulum/tz/timezone.py new file mode 100644 index 0000000..6281013 --- /dev/null +++ b/pendulum/tz/timezone.py @@ -0,0 +1,377 @@ +from datetime import datetime +from datetime import timedelta +from datetime import tzinfo +from typing import Optional +from typing import TypeVar +from typing import overload + +import pendulum + +from pendulum.helpers import local_time +from pendulum.helpers import timestamp +from pendulum.utils._compat import _HAS_FOLD + +from .exceptions import AmbiguousTime +from .exceptions import NonExistingTime +from .zoneinfo import read +from .zoneinfo import read_file +from .zoneinfo.transition import Transition + + +POST_TRANSITION = "post" +PRE_TRANSITION = "pre" +TRANSITION_ERROR = "error" + +_datetime = datetime +_D = TypeVar("_D", bound=datetime) + + +class Timezone(tzinfo): + """ + Represents a named timezone. + + The accepted names are those provided by the IANA time zone database. + + >>> from pendulum.tz.timezone import Timezone + >>> tz = Timezone('Europe/Paris') + """ + + def __init__(self, name, extended=True): # type: (str, bool) -> None + tz = read(name, extend=extended) + + self._name = name + self._transitions = tz.transitions + self._hint = {True: None, False: None} + + @property + def name(self): # type: () -> str + return self._name + + def convert(self, dt, dst_rule=None): # type: (_D, Optional[str]) -> _D + """ + Converts a datetime in the current timezone. + + If the datetime is naive, it will be normalized. + + >>> from datetime import datetime + >>> from pendulum import timezone + >>> paris = timezone('Europe/Paris') + >>> dt = datetime(2013, 3, 31, 2, 30, fold=1) + >>> in_paris = paris.convert(dt) + >>> in_paris.isoformat() + '2013-03-31T03:30:00+02:00' + + If the datetime is aware, it will be properly converted. + + >>> new_york = timezone('America/New_York') + >>> in_new_york = new_york.convert(in_paris) + >>> in_new_york.isoformat() + '2013-03-30T21:30:00-04:00' + """ + if dt.tzinfo is None: + return self._normalize(dt, dst_rule=dst_rule) + + return self._convert(dt) + + def datetime( + self, year, month, day, hour=0, minute=0, second=0, microsecond=0 + ): # type: (int, int, int, int, int, int, int) -> _datetime + """ + Return a normalized datetime for the current timezone. + """ + if _HAS_FOLD: + return self.convert( + datetime(year, month, day, hour, minute, second, microsecond, fold=1) + ) + + return self.convert( + datetime(year, month, day, hour, minute, second, microsecond), + dst_rule=POST_TRANSITION, + ) + + def _normalize(self, dt, dst_rule=None): # type: (_D, Optional[str]) -> _D + sec = timestamp(dt) + fold = 0 + transition = self._lookup_transition(sec) + + if not _HAS_FOLD and dst_rule is None: + dst_rule = POST_TRANSITION + + if dst_rule is None: + dst_rule = PRE_TRANSITION + if dt.fold == 1: + dst_rule = POST_TRANSITION + + if sec < transition.local: + if transition.is_ambiguous(sec): + # Ambiguous time + if dst_rule == TRANSITION_ERROR: + raise AmbiguousTime(dt) + + # We set the fold attribute for later + if dst_rule == POST_TRANSITION: + fold = 1 + elif transition.previous is not None: + transition = transition.previous + + if transition: + if transition.is_ambiguous(sec): + # Ambiguous time + if dst_rule == TRANSITION_ERROR: + raise AmbiguousTime(dt) + + # We set the fold attribute for later + if dst_rule == POST_TRANSITION: + fold = 1 + elif transition.is_missing(sec): + # Skipped time + if dst_rule == TRANSITION_ERROR: + raise NonExistingTime(dt) + + # We adjust accordingly + if dst_rule == POST_TRANSITION: + sec += transition.fix + fold = 1 + else: + sec -= transition.fix + + kwargs = {"tzinfo": self} + if _HAS_FOLD or isinstance(dt, pendulum.DateTime): + kwargs["fold"] = fold + + return dt.__class__(*local_time(sec, 0, dt.microsecond), **kwargs) + + def _convert(self, dt): # type: (_D) -> _D + if dt.tzinfo is self: + return self._normalize(dt, dst_rule=POST_TRANSITION) + + if not isinstance(dt.tzinfo, Timezone): + return dt.astimezone(self) + + stamp = timestamp(dt) + + if isinstance(dt.tzinfo, FixedTimezone): + offset = dt.tzinfo.offset + else: + transition = dt.tzinfo._lookup_transition(stamp) + offset = transition.ttype.offset + + if stamp < transition.local and transition.previous is not None: + if ( + transition.previous.is_ambiguous(stamp) + and getattr(dt, "fold", 1) == 0 + ): + pass + else: + offset = transition.previous.ttype.offset + + stamp -= offset + + transition = self._lookup_transition(stamp, is_utc=True) + if stamp < transition.at and transition.previous is not None: + transition = transition.previous + + offset = transition.ttype.offset + stamp += offset + fold = int(not transition.ttype.is_dst()) + + kwargs = {"tzinfo": self} + + if _HAS_FOLD or isinstance(dt, pendulum.DateTime): + kwargs["fold"] = fold + + return dt.__class__(*local_time(stamp, 0, dt.microsecond), **kwargs) + + def _lookup_transition( + self, stamp, is_utc=False + ): # type: (int, bool) -> Transition + lo, hi = 0, len(self._transitions) + hint = self._hint[is_utc] + if hint: + if stamp == hint[0]: + return self._transitions[hint[1]] + elif stamp < hint[0]: + hi = hint[1] + else: + lo = hint[1] + + if not is_utc: + while lo < hi: + mid = (lo + hi) // 2 + if stamp < self._transitions[mid].to: + hi = mid + else: + lo = mid + 1 + else: + while lo < hi: + mid = (lo + hi) // 2 + if stamp < self._transitions[mid].at: + hi = mid + else: + lo = mid + 1 + + if lo >= len(self._transitions): + # Beyond last transition + lo = len(self._transitions) - 1 + + self._hint[is_utc] = (stamp, lo) + + return self._transitions[lo] + + @overload + def utcoffset(self, dt): # type: (None) -> None + pass + + @overload + def utcoffset(self, dt): # type: (_datetime) -> timedelta + pass + + def utcoffset(self, dt): + if dt is None: + return + + transition = self._get_transition(dt) + + return transition.utcoffset() + + def dst( + self, dt # type: Optional[_datetime] + ): # type: (...) -> Optional[timedelta] + if dt is None: + return + + transition = self._get_transition(dt) + + if not transition.ttype.is_dst(): + return timedelta() + + return timedelta(seconds=transition.fix) + + def tzname(self, dt): # type: (Optional[_datetime]) -> Optional[str] + if dt is None: + return + + transition = self._get_transition(dt) + + return transition.ttype.abbreviation + + def _get_transition(self, dt): # type: (_datetime) -> Transition + if dt.tzinfo is not None and dt.tzinfo is not self: + dt = dt - dt.utcoffset() + + stamp = timestamp(dt) + + transition = self._lookup_transition(stamp, is_utc=True) + else: + stamp = timestamp(dt) + + transition = self._lookup_transition(stamp) + + if stamp < transition.local and transition.previous is not None: + fold = getattr(dt, "fold", 1) + if transition.is_ambiguous(stamp): + if fold == 0: + transition = transition.previous + elif transition.previous.is_ambiguous(stamp) and fold == 0: + pass + else: + transition = transition.previous + + return transition + + def fromutc(self, dt): # type: (_D) -> _D + stamp = timestamp(dt) + + transition = self._lookup_transition(stamp, is_utc=True) + if stamp < transition.at and transition.previous is not None: + transition = transition.previous + + stamp += transition.ttype.offset + + return dt.__class__(*local_time(stamp, 0, dt.microsecond), tzinfo=self) + + def __repr__(self): # type: () -> str + return "Timezone('{}')".format(self._name) + + def __getinitargs__(self): # type: () -> tuple + return (self._name,) + + +class FixedTimezone(Timezone): + def __init__(self, offset, name=None): + sign = "-" if offset < 0 else "+" + + minutes = offset / 60 + hour, minute = divmod(abs(int(minutes)), 60) + + if not name: + name = "{0}{1:02d}:{2:02d}".format(sign, hour, minute) + + self._name = name + self._offset = offset + self._utcoffset = timedelta(seconds=offset) + + @property + def offset(self): # type: () -> int + return self._offset + + def _normalize(self, dt, dst_rule=None): # type: (_D, Optional[str]) -> _D + if _HAS_FOLD: + dt = dt.__class__( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=self, + fold=0, + ) + else: + dt = dt.__class__( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=self, + ) + + return dt + + def _convert(self, dt): # type: (_D) -> _D + if dt.tzinfo is not self: + return dt.astimezone(self) + + return dt + + def utcoffset(self, dt): # type: (Optional[_datetime]) -> timedelta + return self._utcoffset + + def dst(self, dt): # type: (Optional[_datetime]) -> timedelta + return timedelta() + + def fromutc(self, dt): # type: (_D) -> _D + # Use the stdlib datetime's add method to avoid infinite recursion + return (datetime.__add__(dt, self._utcoffset)).replace(tzinfo=self) + + def tzname(self, dt): # type: (Optional[_datetime]) -> Optional[str] + return self._name + + def __getinitargs__(self): # type: () -> tuple + return self._offset, self._name + + +class TimezoneFile(Timezone): + def __init__(self, path): + tz = read_file(path) + + self._name = "" + self._transitions = tz.transitions + self._hint = {True: None, False: None} + + +UTC = FixedTimezone(0, "UTC") diff --git a/pendulum/tz/zoneinfo/__init__.py b/pendulum/tz/zoneinfo/__init__.py new file mode 100644 index 0000000..c183365 --- /dev/null +++ b/pendulum/tz/zoneinfo/__init__.py @@ -0,0 +1,16 @@ +from .reader import Reader +from .timezone import Timezone + + +def read(name, extend=True): # type: (str, bool) -> Timezone + """ + Read the zoneinfo structure for a given timezone name. + """ + return Reader(extend=extend).read_for(name) + + +def read_file(path, extend=True): # type: (str, bool) -> Timezone + """ + Read the zoneinfo structure for a given path. + """ + return Reader(extend=extend).read(path) diff --git a/pendulum/tz/zoneinfo/exceptions.py b/pendulum/tz/zoneinfo/exceptions.py new file mode 100644 index 0000000..5412181 --- /dev/null +++ b/pendulum/tz/zoneinfo/exceptions.py @@ -0,0 +1,18 @@ +class ZoneinfoError(Exception): + + pass + + +class InvalidZoneinfoFile(ZoneinfoError): + + pass + + +class InvalidTimezone(ZoneinfoError): + def __init__(self, name): + super(InvalidTimezone, self).__init__('Invalid timezone "{}"'.format(name)) + + +class InvalidPosixSpec(ZoneinfoError): + def __init__(self, spec): + super(InvalidPosixSpec, self).__init__("Invalid POSIX spec: {}".format(spec)) diff --git a/pendulum/tz/zoneinfo/posix_timezone.py b/pendulum/tz/zoneinfo/posix_timezone.py new file mode 100644 index 0000000..74a32eb --- /dev/null +++ b/pendulum/tz/zoneinfo/posix_timezone.py @@ -0,0 +1,270 @@ +""" +Parsing of a POSIX zone spec as described in the TZ part of section 8.3 in +http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html. +""" +import re + +from typing import Optional + +from pendulum.constants import MONTHS_OFFSETS +from pendulum.constants import SECS_PER_DAY + +from .exceptions import InvalidPosixSpec + + +_spec = re.compile( + "^" + r"(?P<std_abbr><.*?>|[^-+,\d]{3,})" + r"(?P<std_offset>([+-])?(\d{1,2})(:\d{2}(:\d{2})?)?)" + r"(?P<dst_info>" + r" (?P<dst_abbr><.*?>|[^-+,\d]{3,})" + r" (?P<dst_offset>([+-])?(\d{1,2})(:\d{2}(:\d{2})?)?)?" + r")?" + r"(?:,(?P<rules>" + r" (?P<dst_start>" + r" (?:J\d+|\d+|M\d{1,2}.\d.[0-6])" + r" (?:/(?P<dst_start_offset>([+-])?(\d+)(:\d{2}(:\d{2})?)?))?" + " )" + " ," + r" (?P<dst_end>" + r" (?:J\d+|\d+|M\d{1,2}.\d.[0-6])" + r" (?:/(?P<dst_end_offset>([+-])?(\d+)(:\d{2}(:\d{2})?)?))?" + " )" + "))?" + "$", + re.VERBOSE, +) + + +def posix_spec(spec): # type: (str) -> PosixTimezone + try: + return _posix_spec(spec) + except ValueError: + raise InvalidPosixSpec(spec) + + +def _posix_spec(spec): # type: (str) -> PosixTimezone + m = _spec.match(spec) + if not m: + raise ValueError("Invalid posix spec") + + std_abbr = _parse_abbr(m.group("std_abbr")) + std_offset = _parse_offset(m.group("std_offset")) + + dst_abbr = None + dst_offset = None + if m.group("dst_info"): + dst_abbr = _parse_abbr(m.group("dst_abbr")) + if m.group("dst_offset"): + dst_offset = _parse_offset(m.group("dst_offset")) + else: + dst_offset = std_offset + 3600 + + dst_start = None + dst_end = None + if m.group("rules"): + dst_start = _parse_rule(m.group("dst_start")) + dst_end = _parse_rule(m.group("dst_end")) + + return PosixTimezone(std_abbr, std_offset, dst_abbr, dst_offset, dst_start, dst_end) + + +def _parse_abbr(text): # type: (str) -> str + return text.lstrip("<").rstrip(">") + + +def _parse_offset(text, sign=-1): # type: (str, int) -> int + if text.startswith(("+", "-")): + if text.startswith("-"): + sign *= -1 + + text = text[1:] + + minutes = 0 + seconds = 0 + + parts = text.split(":") + hours = int(parts[0]) + + if len(parts) > 1: + minutes = int(parts[1]) + + if len(parts) > 2: + seconds = int(parts[2]) + + return sign * ((((hours * 60) + minutes) * 60) + seconds) + + +def _parse_rule(rule): # type: (str) -> PosixTransition + klass = NPosixTransition + args = () + + if rule.startswith("M"): + rule = rule[1:] + parts = rule.split(".") + month = int(parts[0]) + week = int(parts[1]) + day = int(parts[2].split("/")[0]) + + args += (month, week, day) + klass = MPosixTransition + elif rule.startswith("J"): + rule = rule[1:] + args += (int(rule.split("/")[0]),) + klass = JPosixTransition + else: + args += (int(rule.split("/")[0]),) + + # Checking offset + parts = rule.split("/") + if len(parts) > 1: + offset = _parse_offset(parts[-1], sign=1) + else: + offset = 7200 + + args += (offset,) + + return klass(*args) + + +class PosixTransition(object): + def __init__(self, offset): # type: (int) -> None + self._offset = offset + + @property + def offset(self): # type: () -> int + return self._offset + + def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int + raise NotImplementedError() + + +class JPosixTransition(PosixTransition): + def __init__(self, day, offset): # type: (int, int) -> None + self._day = day + + super(JPosixTransition, self).__init__(offset) + + @property + def day(self): # type: () -> int + """ + day of non-leap year [1:365] + """ + return self._day + + def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int + days = self._day + if not is_leap or days < MONTHS_OFFSETS[1][3]: + days -= 1 + + return (days * SECS_PER_DAY) + self._offset + + +class NPosixTransition(PosixTransition): + def __init__(self, day, offset): # type: (int, int) -> None + self._day = day + + super(NPosixTransition, self).__init__(offset) + + @property + def day(self): # type: () -> int + """ + day of year [0:365] + """ + return self._day + + def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int + days = self._day + + return (days * SECS_PER_DAY) + self._offset + + +class MPosixTransition(PosixTransition): + def __init__(self, month, week, weekday, offset): + # type: (int, int, int, int) -> None + self._month = month + self._week = week + self._weekday = weekday + + super(MPosixTransition, self).__init__(offset) + + @property + def month(self): # type: () -> int + """ + month of year [1:12] + """ + return self._month + + @property + def week(self): # type: () -> int + """ + week of month [1:5] (5==last) + """ + return self._week + + @property + def weekday(self): # type: () -> int + """ + 0==Sun, ..., 6=Sat + """ + return self._weekday + + def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int + last_week = self._week == 5 + days = MONTHS_OFFSETS[is_leap][self._month + int(last_week)] + weekday = (jan1_weekday + days) % 7 + if last_week: + days -= (weekday + 7 - 1 - self._weekday) % 7 + 1 + else: + days += (self._weekday + 7 - weekday) % 7 + days += (self._week - 1) * 7 + + return (days * SECS_PER_DAY) + self._offset + + +class PosixTimezone: + """ + The entirety of a POSIX-string specified time-zone rule. + + The standard abbreviation and offset are always given. + """ + + def __init__( + self, + std_abbr, # type: str + std_offset, # type: int + dst_abbr, # type: Optional[str] + dst_offset, # type: Optional[int] + dst_start=None, # type: Optional[PosixTransition] + dst_end=None, # type: Optional[PosixTransition] + ): + self._std_abbr = std_abbr + self._std_offset = std_offset + self._dst_abbr = dst_abbr + self._dst_offset = dst_offset + self._dst_start = dst_start + self._dst_end = dst_end + + @property + def std_abbr(self): # type: () -> str + return self._std_abbr + + @property + def std_offset(self): # type: () -> int + return self._std_offset + + @property + def dst_abbr(self): # type: () -> Optional[str] + return self._dst_abbr + + @property + def dst_offset(self): # type: () -> Optional[int] + return self._dst_offset + + @property + def dst_start(self): # type: () -> Optional[PosixTransition] + return self._dst_start + + @property + def dst_end(self): # type: () -> Optional[PosixTransition] + return self._dst_end diff --git a/pendulum/tz/zoneinfo/reader.py b/pendulum/tz/zoneinfo/reader.py new file mode 100644 index 0000000..f4c1fa6 --- /dev/null +++ b/pendulum/tz/zoneinfo/reader.py @@ -0,0 +1,224 @@ +import os + +from collections import namedtuple +from struct import unpack +from typing import IO +from typing import Any +from typing import Dict +from typing import List +from typing import Optional +from typing import Tuple + +import pytzdata + +from pytzdata.exceptions import TimezoneNotFound + +from pendulum.utils._compat import PY2 + +from .exceptions import InvalidTimezone +from .exceptions import InvalidZoneinfoFile +from .posix_timezone import PosixTimezone +from .posix_timezone import posix_spec +from .timezone import Timezone +from .transition import Transition +from .transition_type import TransitionType + + +_offset = namedtuple("offset", "utc_total_offset is_dst abbr_idx") + +header = namedtuple( + "header", + "version " "utclocals " "stdwalls " "leaps " "transitions " "types " "abbr_size", +) + + +class Reader: + """ + Reads compiled zoneinfo TZif (\0, 2 or 3) files. + """ + + def __init__(self, extend=True): # type: (bool) -> None + self._extend = extend + + def read_for(self, timezone): # type: (str) -> Timezone + """ + Read the zoneinfo structure for a given timezone name. + + :param timezone: The timezone. + """ + try: + file_path = pytzdata.tz_path(timezone) + except TimezoneNotFound: + raise InvalidTimezone(timezone) + + return self.read(file_path) + + def read(self, file_path): # type: (str) -> Timezone + """ + Read a zoneinfo structure from the given path. + + :param file_path: The path of a zoneinfo file. + """ + if not os.path.exists(file_path): + raise InvalidZoneinfoFile("The tzinfo file does not exist") + + with open(file_path, "rb") as fd: + return self._parse(fd) + + def _check_read(self, fd, nbytes): # type: (...) -> bytes + """ + Reads the given number of bytes from the given file + and checks that the correct number of bytes could be read. + """ + result = fd.read(nbytes) + + if (not result and nbytes > 0) or len(result) != nbytes: + raise InvalidZoneinfoFile( + "Expected {} bytes reading {}, " + "but got {}".format(nbytes, fd.name, len(result) if result else 0) + ) + + if PY2: + return bytearray(result) + + return result + + def _parse(self, fd): # type: (...) -> Timezone + """ + Parse a zoneinfo file. + """ + hdr = self._parse_header(fd) + + if hdr.version in (2, 3): + # We're skipping the entire v1 file since + # at least the same data will be found in TZFile 2. + fd.seek( + hdr.transitions * 5 + + hdr.types * 6 + + hdr.abbr_size + + hdr.leaps * 4 + + hdr.stdwalls + + hdr.utclocals, + 1, + ) + + # Parse the second header + hdr = self._parse_header(fd) + + if hdr.version != 2 and hdr.version != 3: + raise InvalidZoneinfoFile( + "Header versions mismatch for file {}".format(fd.name) + ) + + # Parse the v2 data + trans = self._parse_trans_64(fd, hdr.transitions) + type_idx = self._parse_type_idx(fd, hdr.transitions) + types = self._parse_types(fd, hdr.types) + abbrs = self._parse_abbrs(fd, hdr.abbr_size, types) + + fd.seek(hdr.leaps * 8 + hdr.stdwalls + hdr.utclocals, 1) + + trule = self._parse_posix_tz(fd) + else: + # TZFile v1 + trans = self._parse_trans_32(fd, hdr.transitions) + type_idx = self._parse_type_idx(fd, hdr.transitions) + types = self._parse_types(fd, hdr.types) + abbrs = self._parse_abbrs(fd, hdr.abbr_size, types) + trule = None + + types = [ + TransitionType(off, is_dst, abbrs[abbr]) for off, is_dst, abbr in types + ] + + transitions = [] + previous = None + for trans, idx in zip(trans, type_idx): + transition = Transition(trans, types[idx], previous) + transitions.append(transition) + + previous = transition + + if not transitions: + transitions.append(Transition(0, types[0], None)) + + return Timezone(transitions, posix_rule=trule, extended=self._extend) + + def _parse_header(self, fd): # type: (...) -> header + buff = self._check_read(fd, 44) + + if buff[:4] != b"TZif": + raise InvalidZoneinfoFile( + 'The file "{}" has an invalid header.'.format(fd.name) + ) + + version = {0x00: 1, 0x32: 2, 0x33: 3}.get(buff[4]) + + if version is None: + raise InvalidZoneinfoFile( + 'The file "{}" has an invalid version.'.format(fd.name) + ) + + hdr = header(version, *unpack(">6l", buff[20:44])) + + return hdr + + def _parse_trans_64(self, fd, n): # type: (IO[Any], int) -> List[int] + trans = [] + for _ in range(n): + buff = self._check_read(fd, 8) + trans.append(unpack(">q", buff)[0]) + + return trans + + def _parse_trans_32(self, fd, n): # type: (IO[Any], int) -> List[int] + trans = [] + for _ in range(n): + buff = self._check_read(fd, 4) + trans.append(unpack(">i", buff)[0]) + + return trans + + def _parse_type_idx(self, fd, n): # type: (IO[Any], int) -> List[int] + buff = self._check_read(fd, n) + + return list(unpack("{}B".format(n), buff)) + + def _parse_types( + self, fd, n + ): # type: (IO[Any], int) -> List[Tuple[Any, bool, int]] + types = [] + + for _ in range(n): + buff = self._check_read(fd, 6) + offset = unpack(">l", buff[:4])[0] + is_dst = buff[4] == 1 + types.append((offset, is_dst, buff[5])) + + return types + + def _parse_abbrs( + self, fd, n, types + ): # type: (IO[Any], int, List[Tuple[Any, bool, int]]) -> Dict[int, str] + abbrs = {} + buff = self._check_read(fd, n) + + for offset, is_dst, idx in types: + if idx not in abbrs: + abbr = buff[idx : buff.find(b"\0", idx)].decode("utf-8") + abbrs[idx] = abbr + + return abbrs + + def _parse_posix_tz(self, fd): # type: (...) -> Optional[PosixTimezone] + s = fd.read().decode("utf-8") + + if not s.startswith("\n") or not s.endswith("\n"): + raise InvalidZoneinfoFile('Invalid posix rule in file "{}"'.format(fd.name)) + + s = s.strip() + + if not s: + return + + return posix_spec(s) diff --git a/pendulum/tz/zoneinfo/timezone.py b/pendulum/tz/zoneinfo/timezone.py new file mode 100644 index 0000000..abdb0ec --- /dev/null +++ b/pendulum/tz/zoneinfo/timezone.py @@ -0,0 +1,128 @@ +from datetime import datetime +from typing import List +from typing import Optional + +from pendulum.constants import DAYS_PER_YEAR +from pendulum.constants import SECS_PER_YEAR +from pendulum.helpers import is_leap +from pendulum.helpers import local_time +from pendulum.helpers import timestamp +from pendulum.helpers import week_day + +from .posix_timezone import PosixTimezone +from .transition import Transition +from .transition_type import TransitionType + + +class Timezone: + def __init__( + self, + transitions, # type: List[Transition] + posix_rule=None, # type: Optional[PosixTimezone] + extended=True, # type: bool + ): + self._posix_rule = posix_rule + self._transitions = transitions + + if extended: + self._extends() + + @property + def transitions(self): # type: () -> List[Transition] + return self._transitions + + @property + def posix_rule(self): + return self._posix_rule + + def _extends(self): + if not self._posix_rule: + return + + posix = self._posix_rule + + if not posix.dst_abbr: + # std only + # The future specification should match the last/default transition + ttype = self._transitions[-1].ttype + if not self._check_ttype(ttype, posix.std_offset, False, posix.std_abbr): + raise ValueError("Posix spec does not match last transition") + + return + + if len(self._transitions) < 2: + raise ValueError("Too few transitions for POSIX spec") + + # Extend the transitions for an additional 400 years + # using the future specification + + # The future specification should match the last two transitions, + # and those transitions should have different is_dst flags. + tr0 = self._transitions[-1] + tr1 = self._transitions[-2] + tt0 = tr0.ttype + tt1 = tr1.ttype + if tt0.is_dst(): + dst = tt0 + std = tt1 + else: + dst = tt1 + std = tt0 + + self._check_ttype(dst, posix.dst_offset, True, posix.dst_abbr) + self._check_ttype(std, posix.std_offset, False, posix.std_abbr) + + # Add the transitions to tr1 and back to tr0 for each extra year. + last_year = local_time(tr0.local, 0, 0)[0] + leap_year = is_leap(last_year) + jan1 = datetime(last_year, 1, 1) + jan1_time = timestamp(jan1) + jan1_weekday = week_day(jan1.year, jan1.month, jan1.day) % 7 + + if local_time(tr1.local, 0, 0)[0] != last_year: + # Add a single extra transition to align to a calendar year. + if tt0.is_dst(): + pt1 = posix.dst_end + else: + pt1 = posix.dst_start + + tr1_offset = pt1.trans_offset(leap_year, jan1_weekday) + tr = Transition(jan1_time + tr1_offset - tt0.offset, tr1.ttype, tr0) + tr0 = tr + tr1 = tr0 + tt0 = tr0.ttype + tt1 = tr1.ttype + + if tt0.is_dst(): + pt1 = posix.dst_end + pt0 = posix.dst_start + else: + pt1 = posix.dst_start + pt0 = posix.dst_end + + tr = tr0 + for year in range(last_year + 1, last_year + 401): + jan1_time += SECS_PER_YEAR[leap_year] + jan1_weekday = (jan1_weekday + DAYS_PER_YEAR[leap_year]) % 7 + leap_year = not leap_year and is_leap(year) + + tr1_offset = pt1.trans_offset(leap_year, jan1_weekday) + tr = Transition(jan1_time + tr1_offset - tt0.offset, tt1, tr) + self._transitions.append(tr) + + tr0_offset = pt0.trans_offset(leap_year, jan1_weekday) + tr = Transition(jan1_time + tr0_offset - tt1.offset, tt0, tr) + self._transitions.append(tr) + + def _check_ttype( + self, + ttype, # type: TransitionType + offset, # type: int + is_dst, # type: bool + abbr, # type: str + ): # type: (...) -> bool + return ( + ttype.offset == offset + and ttype.is_dst() == is_dst + and ttype.abbreviation == abbr + ) diff --git a/pendulum/tz/zoneinfo/transition.py b/pendulum/tz/zoneinfo/transition.py new file mode 100644 index 0000000..dcbd5d3 --- /dev/null +++ b/pendulum/tz/zoneinfo/transition.py @@ -0,0 +1,77 @@ +from datetime import timedelta +from typing import Optional + +from .transition_type import TransitionType + + +class Transition: + def __init__( + self, + at, # type: int + ttype, # type: TransitionType + previous, # type: Optional[Transition] + ): + self._at = at + + if previous: + self._local = at + previous.ttype.offset + else: + self._local = at + ttype.offset + + self._ttype = ttype + self._previous = previous + + if self.previous: + self._fix = self._ttype.offset - self.previous.ttype.offset + else: + self._fix = 0 + + self._to = self._local + self._fix + self._to_utc = self._at + self._fix + self._utcoffset = timedelta(seconds=ttype.offset) + + @property + def at(self): # type: () -> int + return self._at + + @property + def local(self): # type: () -> int + return self._local + + @property + def to(self): # type: () -> int + return self._to + + @property + def to_utc(self): # type: () -> int + return self._to + + @property + def ttype(self): # type: () -> TransitionType + return self._ttype + + @property + def previous(self): # type: () -> Optional[Transition] + return self._previous + + @property + def fix(self): # type: () -> int + return self._fix + + def is_ambiguous(self, stamp): # type: (int) -> bool + return self._to <= stamp < self._local + + def is_missing(self, stamp): # type: (int) -> bool + return self._local <= stamp < self._to + + def utcoffset(self): # type: () -> timedelta + return self._utcoffset + + def __contains__(self, stamp): # type: (int) -> bool + if self.previous is None: + return stamp < self.local + + return self.previous.local <= stamp < self.local + + def __repr__(self): # type: () -> str + return "Transition({} -> {}, {})".format(self._local, self._to, self._ttype) diff --git a/pendulum/tz/zoneinfo/transition_type.py b/pendulum/tz/zoneinfo/transition_type.py new file mode 100644 index 0000000..c2c33c6 --- /dev/null +++ b/pendulum/tz/zoneinfo/transition_type.py @@ -0,0 +1,35 @@ +from datetime import timedelta + +from pendulum.utils._compat import PY2 +from pendulum.utils._compat import encode + + +class TransitionType: + def __init__(self, offset, is_dst, abbr): + self._offset = offset + self._is_dst = is_dst + self._abbr = abbr + + self._utcoffset = timedelta(seconds=offset) + + @property + def offset(self): # type: () -> int + return self._offset + + @property + def abbreviation(self): # type: () -> str + if PY2: + return encode(self._abbr) + + return self._abbr + + def is_dst(self): # type: () -> bool + return self._is_dst + + def utcoffset(self): # type: () -> timedelta + return self._utcoffset + + def __repr__(self): # type: () -> str + return "TransitionType({}, {}, {})".format( + self._offset, self._is_dst, self._abbr + ) diff --git a/pendulum/utils/__init__.py b/pendulum/utils/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pendulum/utils/__init__.py diff --git a/pendulum/utils/_compat.py b/pendulum/utils/_compat.py new file mode 100644 index 0000000..4893979 --- /dev/null +++ b/pendulum/utils/_compat.py @@ -0,0 +1,54 @@ +import sys + + +PY2 = sys.version_info < (3, 0) +PY36 = sys.version_info >= (3, 6) +PYPY = hasattr(sys, "pypy_version_info") + +_HAS_FOLD = PY36 + + +try: # Python 2 + long = long + unicode = unicode + basestring = basestring +except NameError: # Python 3 + long = int + unicode = str + basestring = str + + +def decode(string, encodings=None): + if not PY2 and not isinstance(string, bytes): + return string + + if PY2 and isinstance(string, unicode): + return string + + encodings = encodings or ["utf-8", "latin1", "ascii"] + + for encoding in encodings: + try: + return string.decode(encoding) + except (UnicodeEncodeError, UnicodeDecodeError): + pass + + return string.decode(encodings[0], errors="ignore") + + +def encode(string, encodings=None): + if not PY2 and isinstance(string, bytes): + return string + + if PY2 and isinstance(string, str): + return string + + encodings = encodings or ["utf-8", "latin1", "ascii"] + + for encoding in encodings: + try: + return string.encode(encoding) + except (UnicodeEncodeError, UnicodeDecodeError): + pass + + return string.encode(encodings[0], errors="ignore") diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..b9120f4 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1128 @@ +[[package]] +category = "dev" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.4" + +[[package]] +category = "dev" +description = "A few extensions to pyyaml." +name = "aspy.yaml" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.3.0" + +[package.dependencies] +pyyaml = "*" + +[[package]] +category = "dev" +description = "Atomic file writes." +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.0" + +[[package]] +category = "dev" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.3.0" + +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + +[[package]] +category = "dev" +description = "Internationalization utilities" +name = "babel" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8.0" + +[package.dependencies] +pytz = ">=2015.7" + +[[package]] +category = "dev" +description = "Backport of functools.lru_cache" +name = "backports.functools-lru-cache" +optional = false +python-versions = ">=2.6" +version = "1.6.1" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black-multipy", "pytest-cov"] + +[[package]] +category = "dev" +description = "The uncompromising code formatter." +name = "black" +optional = false +python-versions = ">=3.6" +version = "19.10b0" + +[package.dependencies] +appdirs = "*" +attrs = ">=18.1.0" +click = ">=6.5" +pathspec = ">=0.6,<1" +regex = "*" +toml = ">=0.9.4" +typed-ast = ">=1.4.0" + +[package.extras] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +category = "dev" +description = "Validate configuration and produce human readable error messages." +name = "cfgv" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.0.1" + +[package.dependencies] +six = "*" + +[[package]] +category = "dev" +description = "Cleo allows you to create beautiful and testable command-line interfaces." +name = "cleo" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.8.1" + +[package.dependencies] +clikit = ">=0.6.0,<0.7.0" + +[[package]] +category = "dev" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.2" + +[[package]] +category = "dev" +description = "CliKit is a group of utilities to build beautiful and testable command line interfaces." +name = "clikit" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.6.2" + +[package.dependencies] +pastel = ">=0.2.0,<0.3.0" +pylev = ">=1.3,<2.0" +crashtest = {version = ">=0.3.0,<0.4.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} +enum34 = {version = ">=1.1,<2.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} +typing = {version = ">=3.6,<4.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\""} +typing-extensions = {version = ">=3.6,<4.0", markers = "python_version >= \"3.5\" and python_full_version < \"3.5.4\""} + +[[package]] +category = "dev" +description = "Cross-platform colored terminal text." +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" + +[[package]] +category = "dev" +description = "Updated configparser from Python 3.7 for Python 2.6+." +name = "configparser" +optional = false +python-versions = ">=2.6" +version = "4.0.2" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2)", "pytest-flake8", "pytest-black-multipy"] + +[[package]] +category = "dev" +description = "Backports and enhancements for the contextlib module" +name = "contextlib2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.6.0.post1" + +[[package]] +category = "dev" +description = "Code coverage measurement for Python" +name = "coverage" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "5.2" + +[package.extras] +toml = ["toml"] + +[[package]] +category = "dev" +description = "Manage Python errors with ease" +name = "crashtest" +optional = false +python-versions = ">=3.6,<4.0" +version = "0.3.0" + +[[package]] +category = "dev" +description = "Distribution utilities" +name = "distlib" +optional = false +python-versions = "*" +version = "0.3.1" + +[[package]] +category = "dev" +description = "Python 3.4 Enum backported to 3.3, 3.2, 3.1, 2.7, 2.6, 2.5, and 2.4" +name = "enum34" +optional = false +python-versions = "*" +version = "1.1.10" + +[[package]] +category = "dev" +description = "A platform independent file lock." +name = "filelock" +optional = false +python-versions = "*" +version = "3.0.12" + +[[package]] +category = "dev" +description = "Let your Python tests travel through time" +name = "freezegun" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.3.15" + +[package.dependencies] +python-dateutil = ">=1.0,<2.0 || >2.0" +six = "*" + +[[package]] +category = "dev" +description = "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+" +name = "funcsigs" +optional = false +python-versions = "*" +version = "1.0.2" + +[[package]] +category = "dev" +description = "Backport of the concurrent.futures package from Python 3.2" +name = "futures" +optional = false +python-versions = "*" +version = "3.1.1" + +[[package]] +category = "dev" +description = "File identification library for Python" +name = "identify" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "1.4.23" + +[package.extras] +license = ["editdistance"] + +[[package]] +category = "dev" +description = "Read metadata from Python packages" +name = "importlib-metadata" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.7.0" + +[package.dependencies] +zipp = ">=0.5" +configparser = {version = ">=3.5", markers = "python_version < \"3\""} +contextlib2 = {version = "*", markers = "python_version < \"3\""} +pathlib2 = {version = "*", markers = "python_version < \"3\""} + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] + +[[package]] +category = "dev" +description = "Read resources from Python packages" +name = "importlib-resources" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "3.0.0" + +[package.dependencies] +contextlib2 = {version = "*", markers = "python_version < \"3\""} +pathlib2 = {version = "*", markers = "python_version < \"3\""} +singledispatch = {version = "*", markers = "python_version < \"3.4\""} +typing = {version = "*", markers = "python_version < \"3.5\""} +zipp = {version = ">=0.4", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["sphinx", "rst.linker", "jaraco.packaging"] + +[[package]] +category = "dev" +description = "A Python utility / library to sort Python imports." +name = "isort" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "4.3.21" + +[package.extras] +pipfile = ["pipreqs", "requirementslib"] +pyproject = ["toml"] +requirements = ["pipreqs", "pip-api"] +xdg_home = ["appdirs (>=1.4.0)"] + +[[package]] +category = "dev" +description = "A very fast and expressive template engine." +name = "jinja2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.11.2" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + +[[package]] +category = "dev" +description = "Python LiveReload is an awesome tool for web developers" +name = "livereload" +optional = false +python-versions = "*" +version = "2.6.2" + +[package.dependencies] +six = "*" +tornado = {version = "*", markers = "python_version > \"2.7\""} + +[[package]] +category = "dev" +description = "Python implementation of Markdown." +name = "markdown" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +version = "3.1.1" + +[package.extras] +testing = ["coverage", "pyyaml"] + +[[package]] +category = "dev" +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." +name = "markdown-include" +optional = false +python-versions = "*" +version = "0.5.1" + +[package.dependencies] +markdown = "*" + +[[package]] +category = "dev" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.1.1" + +[[package]] +category = "dev" +description = "Project documentation with Markdown." +name = "mkdocs" +optional = false +python-versions = ">=2.7.9,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.0.4" + +[package.dependencies] +Jinja2 = ">=2.7.1" +Markdown = ">=2.3.1" +PyYAML = ">=3.10" +click = ">=3.3" +livereload = ">=2.5.1" +tornado = ">=5.0" + +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = "*" +version = "5.0.0" + +[package.dependencies] +six = ">=1.0.0,<2.0.0" + +[[package]] +category = "dev" +description = "Node.js virtual environment builder" +name = "nodeenv" +optional = false +python-versions = "*" +version = "1.4.0" + +[[package]] +category = "dev" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.4" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +category = "dev" +description = "Bring colors to your terminal." +name = "pastel" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.2.0" + +[[package]] +category = "dev" +description = "Object-oriented filesystem paths" +name = "pathlib2" +optional = false +python-versions = "*" +version = "2.3.5" + +[package.dependencies] +six = "*" +scandir = {version = "*", markers = "python_version < \"3.5\""} + +[[package]] +category = "dev" +description = "Utility library for gitignore style pattern matching of file paths." +name = "pathspec" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.8.0" + +[[package]] +category = "dev" +description = "Backport of PEP 562." +name = "pep562" +optional = false +python-versions = "*" +version = "1.0" + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.13.1" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +category = "dev" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +name = "pre-commit" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.21.0" + +[package.dependencies] +"aspy.yaml" = "*" +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = "*" +six = "*" +toml = "*" +virtualenv = ">=15.2" +futures = {version = "*", markers = "python_version < \"3.2\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +importlib-resources = {version = "*", markers = "python_version < \"3.7\""} + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.9.0" + +[[package]] +category = "dev" +description = "Pygments is a syntax highlighting package written in Python." +name = "pygments" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.5.2" + +[[package]] +category = "dev" +description = "A pure Python Levenshtein implementation that's not freaking GPL'd." +name = "pylev" +optional = false +python-versions = "*" +version = "1.3.0" + +[[package]] +category = "dev" +description = "Extension pack for Python Markdown." +name = "pymdown-extensions" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +version = "6.2.1" + +[package.dependencies] +Markdown = ">=3.0.1" +pep562 = "*" + +[[package]] +category = "dev" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.7" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "4.6.11" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +six = ">=1.10.0" +wcwidth = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\" and python_version != \"3.4\""} +funcsigs = {version = ">=1.0", markers = "python_version < \"3.0\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +pathlib2 = {version = ">=2.2.0", markers = "python_version < \"3.6\""} + +[[package.dependencies.more-itertools]] +markers = "python_version <= \"2.7\"" +version = ">=4.0.0,<6.0.0" + +[[package.dependencies.more-itertools]] +markers = "python_version > \"2.7\"" +version = ">=4.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "nose", "requests", "mock"] + +[[package]] +category = "dev" +description = "Pytest plugin for measuring coverage." +name = "pytest-cov" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.10.0" + +[package.dependencies] +coverage = ">=4.4" +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] + +[[package]] +category = "main" +description = "Extensions to the standard Python datetime module" +name = "python-dateutil" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +version = "2.8.1" + +[package.dependencies] +six = ">=1.5" + +[[package]] +category = "dev" +description = "World timezone definitions, modern and historical" +name = "pytz" +optional = false +python-versions = "*" +version = "2020.1" + +[[package]] +category = "main" +description = "The Olson timezone database for Python." +name = "pytzdata" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2020.1" + +[[package]] +category = "dev" +description = "YAML parser and emitter for Python" +name = "pyyaml" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "5.3.1" + +[[package]] +category = "dev" +description = "Alternative regular expression module, to replace re." +name = "regex" +optional = false +python-versions = "*" +version = "2020.6.8" + +[[package]] +category = "dev" +description = "scandir, a better directory iterator and faster os.walk()" +name = "scandir" +optional = false +python-versions = "*" +version = "1.10.0" + +[[package]] +category = "dev" +description = "This library brings functools.singledispatch from Python 3.4 to Python 2.6-3.3." +name = "singledispatch" +optional = false +python-versions = "*" +version = "3.4.0.3" + +[package.dependencies] +six = "*" + +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" + +[[package]] +category = "dev" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.1" + +[[package]] +category = "dev" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +name = "tornado" +optional = false +python-versions = ">= 3.5" +version = "6.0.4" + +[[package]] +category = "dev" +description = "tox is a generic virtualenv management and test command line tool" +name = "tox" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "3.16.1" + +[package.dependencies] +filelock = ">=3.0.0" +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" +colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = ">=0.12,<2", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["sphinx (>=2.0.0)", "towncrier (>=18.5.0)", "pygments-github-lexers (>=0.0.5)", "sphinxcontrib-autoprogram (>=0.1.5)"] +testing = ["freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-xdist (>=1.22.2)", "pytest-randomly (>=1.0.0)", "flaky (>=3.4.0)", "psutil (>=5.6.1)"] + +[[package]] +category = "dev" +description = "a fork of Python 2 and 3 ast modules with type comment support" +name = "typed-ast" +optional = false +python-versions = "*" +version = "1.4.1" + +[[package]] +category = "main" +description = "Type Hints for Python" +name = "typing" +optional = false +python-versions = "*" +version = "3.7.4.1" + +[[package]] +category = "dev" +description = "Backported and Experimental Type Hints for Python 3.5+" +name = "typing-extensions" +optional = false +python-versions = "*" +version = "3.7.4.2" + +[[package]] +category = "dev" +description = "Virtual Python Environment builder" +name = "virtualenv" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "20.0.26" + +[package.dependencies] +appdirs = ">=1.4.3,<2" +distlib = ">=0.3.1,<1" +filelock = ">=3.0.0,<4" +six = ">=1.9.0,<2" +importlib-metadata = {version = ">=0.12,<2", markers = "python_version < \"3.8\""} +importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} +pathlib2 = {version = ">=2.3.3,<3", markers = "python_version < \"3.4\" and sys_platform != \"win32\""} + +[package.extras] +docs = ["sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2)"] +testing = ["pytest (>=4)", "coverage (>=5)", "coverage-enable-subprocess (>=1)", "pytest-xdist (>=1.31.0)", "pytest-mock (>=2)", "pytest-env (>=0.6.2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-freezegun (>=0.4.1)", "flaky (>=3)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] + +[[package]] +category = "dev" +description = "Measures the displayed width of unicode strings in a terminal" +name = "wcwidth" +optional = false +python-versions = "*" +version = "0.2.5" + +[package.dependencies] +"backports.functools-lru-cache" = {version = ">=1.2.1", markers = "python_version < \"3.2\""} + +[[package]] +category = "dev" +description = "Backport of pathlib-compatible object wrapper for zip files" +name = "zipp" +optional = false +python-versions = ">=2.7" +version = "1.2.0" + +[package.dependencies] +contextlib2 = {version = "*", markers = "python_version < \"3.4\""} + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pathlib2", "unittest2", "jaraco.itertools", "func-timeout"] + +[metadata] +content-hash = "9a0542f32380e0fef3eb8d37b90903742a3fcad3b7e6b22f6ea8e4faac269e28" +python-versions = "~2.7 || ^3.5" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +"aspy.yaml" = [ + {file = "aspy.yaml-1.3.0-py2.py3-none-any.whl", hash = "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc"}, + {file = "aspy.yaml-1.3.0.tar.gz", hash = "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +babel = [ + {file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"}, + {file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"}, +] +"backports.functools-lru-cache" = [ + {file = "backports.functools_lru_cache-1.6.1-py2.py3-none-any.whl", hash = "sha256:0bada4c2f8a43d533e4ecb7a12214d9420e66eb206d54bf2d682581ca4b80848"}, + {file = "backports.functools_lru_cache-1.6.1.tar.gz", hash = "sha256:8fde5f188da2d593bd5bc0be98d9abc46c95bb8a9dde93429570192ee6cc2d4a"}, +] +black = [ + {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, + {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, +] +cfgv = [ + {file = "cfgv-2.0.1-py2.py3-none-any.whl", hash = "sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289"}, + {file = "cfgv-2.0.1.tar.gz", hash = "sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144"}, +] +cleo = [ + {file = "cleo-0.8.1-py2.py3-none-any.whl", hash = "sha256:141cda6dc94a92343be626bb87a0b6c86ae291dfc732a57bf04310d4b4201753"}, + {file = "cleo-0.8.1.tar.gz", hash = "sha256:3d0e22d30117851b45970b6c14aca4ab0b18b1b53c8af57bed13208147e4069f"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +clikit = [ + {file = "clikit-0.6.2-py2.py3-none-any.whl", hash = "sha256:71268e074e68082306e23d7369a7b99f824a0ef926e55ba2665e911f7208489e"}, + {file = "clikit-0.6.2.tar.gz", hash = "sha256:442ee5db9a14120635c5990bcdbfe7c03ada5898291f0c802f77be71569ded59"}, +] +colorama = [ + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, +] +configparser = [ + {file = "configparser-4.0.2-py2.py3-none-any.whl", hash = "sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c"}, + {file = "configparser-4.0.2.tar.gz", hash = "sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df"}, +] +contextlib2 = [ + {file = "contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"}, + {file = "contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e"}, +] +coverage = [ + {file = "coverage-5.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:d9ad0a988ae20face62520785ec3595a5e64f35a21762a57d115dae0b8fb894a"}, + {file = "coverage-5.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:4bb385a747e6ae8a65290b3df60d6c8a692a5599dc66c9fa3520e667886f2e10"}, + {file = "coverage-5.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:9702e2cb1c6dec01fb8e1a64c015817c0800a6eca287552c47a5ee0ebddccf62"}, + {file = "coverage-5.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:42fa45a29f1059eda4d3c7b509589cc0343cd6bbf083d6118216830cd1a51613"}, + {file = "coverage-5.2-cp27-cp27m-win32.whl", hash = "sha256:41d88736c42f4a22c494c32cc48a05828236e37c991bd9760f8923415e3169e4"}, + {file = "coverage-5.2-cp27-cp27m-win_amd64.whl", hash = "sha256:bbb387811f7a18bdc61a2ea3d102be0c7e239b0db9c83be7bfa50f095db5b92a"}, + {file = "coverage-5.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:3740b796015b889e46c260ff18b84683fa2e30f0f75a171fb10d2bf9fb91fc70"}, + {file = "coverage-5.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ebf2431b2d457ae5217f3a1179533c456f3272ded16f8ed0b32961a6d90e38ee"}, + {file = "coverage-5.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:d54d7ea74cc00482a2410d63bf10aa34ebe1c49ac50779652106c867f9986d6b"}, + {file = "coverage-5.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:87bdc8135b8ee739840eee19b184804e5d57f518578ffc797f5afa2c3c297913"}, + {file = "coverage-5.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ed9a21502e9223f563e071759f769c3d6a2e1ba5328c31e86830368e8d78bc9c"}, + {file = "coverage-5.2-cp35-cp35m-win32.whl", hash = "sha256:509294f3e76d3f26b35083973fbc952e01e1727656d979b11182f273f08aa80b"}, + {file = "coverage-5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:ca63dae130a2e788f2b249200f01d7fa240f24da0596501d387a50e57aa7075e"}, + {file = "coverage-5.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:5c74c5b6045969b07c9fb36b665c9cac84d6c174a809fc1b21bdc06c7836d9a0"}, + {file = "coverage-5.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c32aa13cc3fe86b0f744dfe35a7f879ee33ac0a560684fef0f3e1580352b818f"}, + {file = "coverage-5.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1e58fca3d9ec1a423f1b7f2aa34af4f733cbfa9020c8fe39ca451b6071237405"}, + {file = "coverage-5.2-cp36-cp36m-win32.whl", hash = "sha256:3b2c34690f613525672697910894b60d15800ac7e779fbd0fccf532486c1ba40"}, + {file = "coverage-5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a4d511012beb967a39580ba7d2549edf1e6865a33e5fe51e4dce550522b3ac0e"}, + {file = "coverage-5.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:32ecee61a43be509b91a526819717d5e5650e009a8d5eda8631a59c721d5f3b6"}, + {file = "coverage-5.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6f91b4492c5cde83bfe462f5b2b997cdf96a138f7c58b1140f05de5751623cf1"}, + {file = "coverage-5.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bfcc811883699ed49afc58b1ed9f80428a18eb9166422bce3c31a53dba00fd1d"}, + {file = "coverage-5.2-cp37-cp37m-win32.whl", hash = "sha256:60a3d36297b65c7f78329b80120f72947140f45b5c7a017ea730f9112b40f2ec"}, + {file = "coverage-5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:12eaccd86d9a373aea59869bc9cfa0ab6ba8b1477752110cb4c10d165474f703"}, + {file = "coverage-5.2-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:d82db1b9a92cb5c67661ca6616bdca6ff931deceebb98eecbd328812dab52032"}, + {file = "coverage-5.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:214eb2110217f2636a9329bc766507ab71a3a06a8ea30cdeebb47c24dce5972d"}, + {file = "coverage-5.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8a3decd12e7934d0254939e2bf434bf04a5890c5bf91a982685021786a08087e"}, + {file = "coverage-5.2-cp38-cp38-win32.whl", hash = "sha256:1dcebae667b73fd4aa69237e6afb39abc2f27520f2358590c1b13dd90e32abe7"}, + {file = "coverage-5.2-cp38-cp38-win_amd64.whl", hash = "sha256:f50632ef2d749f541ca8e6c07c9928a37f87505ce3a9f20c8446ad310f1aa87b"}, + {file = "coverage-5.2-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:7403675df5e27745571aba1c957c7da2dacb537c21e14007ec3a417bf31f7f3d"}, + {file = "coverage-5.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:0fc4e0d91350d6f43ef6a61f64a48e917637e1dcfcba4b4b7d543c628ef82c2d"}, + {file = "coverage-5.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:25fe74b5b2f1b4abb11e103bb7984daca8f8292683957d0738cd692f6a7cc64c"}, + {file = "coverage-5.2-cp39-cp39-win32.whl", hash = "sha256:d67599521dff98ec8c34cd9652cbcfe16ed076a2209625fca9dc7419b6370e5c"}, + {file = "coverage-5.2-cp39-cp39-win_amd64.whl", hash = "sha256:10f2a618a6e75adf64329f828a6a5b40244c1c50f5ef4ce4109e904e69c71bd2"}, + {file = "coverage-5.2.tar.gz", hash = "sha256:1874bdc943654ba46d28f179c1846f5710eda3aeb265ff029e0ac2b52daae404"}, +] +crashtest = [ + {file = "crashtest-0.3.0-py3-none-any.whl", hash = "sha256:06069a9267c54be31c42b03574b72407bf780e13c82cb0238f24ea69cf25b6dd"}, + {file = "crashtest-0.3.0.tar.gz", hash = "sha256:e9c06cc96400939ab5327123a3f699078eaad8a6283247d7b2ae0f6afffadf14"}, +] +distlib = [ + {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, + {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, +] +enum34 = [ + {file = "enum34-1.1.10-py2-none-any.whl", hash = "sha256:a98a201d6de3f2ab3db284e70a33b0f896fbf35f8086594e8c9e74b909058d53"}, + {file = "enum34-1.1.10-py3-none-any.whl", hash = "sha256:c3858660960c984d6ab0ebad691265180da2b43f07e061c0f8dca9ef3cffd328"}, + {file = "enum34-1.1.10.tar.gz", hash = "sha256:cce6a7477ed816bd2542d03d53db9f0db935dd013b70f336a95c73979289f248"}, +] +filelock = [ + {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, + {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, +] +freezegun = [ + {file = "freezegun-0.3.15-py2.py3-none-any.whl", hash = "sha256:82c757a05b7c7ca3e176bfebd7d6779fd9139c7cb4ef969c38a28d74deef89b2"}, + {file = "freezegun-0.3.15.tar.gz", hash = "sha256:e2062f2c7f95cc276a834c22f1a17179467176b624cc6f936e8bc3be5535ad1b"}, +] +funcsigs = [ + {file = "funcsigs-1.0.2-py2.py3-none-any.whl", hash = "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca"}, + {file = "funcsigs-1.0.2.tar.gz", hash = "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"}, +] +futures = [ + {file = "futures-3.1.1-py2-none-any.whl", hash = "sha256:c4884a65654a7c45435063e14ae85280eb1f111d94e542396717ba9828c4337f"}, + {file = "futures-3.1.1-py3-none-any.whl", hash = "sha256:3a44f286998ae64f0cc083682fcfec16c406134a81a589a5de445d7bb7c2751b"}, + {file = "futures-3.1.1.tar.gz", hash = "sha256:51ecb45f0add83c806c68e4b06106f90db260585b25ef2abfcda0bd95c0132fd"}, +] +identify = [ + {file = "identify-1.4.23-py2.py3-none-any.whl", hash = "sha256:882c4b08b4569517b5f2257ecca180e01f38400a17f429f5d0edff55530c41c7"}, + {file = "identify-1.4.23.tar.gz", hash = "sha256:f89add935982d5bc62913ceee16c9297d8ff14b226e9d3072383a4e38136b656"}, +] +importlib-metadata = [ + {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, + {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, +] +importlib-resources = [ + {file = "importlib_resources-3.0.0-py2.py3-none-any.whl", hash = "sha256:d028f66b66c0d5732dae86ba4276999855e162a749c92620a38c1d779ed138a7"}, + {file = "importlib_resources-3.0.0.tar.gz", hash = "sha256:19f745a6eca188b490b1428c8d1d4a0d2368759f32370ea8fb89cad2ab1106c3"}, +] +isort = [ + {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, + {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, +] +jinja2 = [ + {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, + {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, +] +livereload = [ + {file = "livereload-2.6.2.tar.gz", hash = "sha256:d1eddcb5c5eb8d2ca1fa1f750e580da624c0f7fcb734aa5780dc81b7dcbd89be"}, +] +markdown = [ + {file = "Markdown-3.1.1-py2.py3-none-any.whl", hash = "sha256:56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c"}, + {file = "Markdown-3.1.1.tar.gz", hash = "sha256:2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a"}, +] +markdown-include = [ + {file = "markdown-include-0.5.1.tar.gz", hash = "sha256:72a45461b589489a088753893bc95c5fa5909936186485f4ed55caa57d10250f"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +mkdocs = [ + {file = "mkdocs-1.0.4-py2.py3-none-any.whl", hash = "sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"}, + {file = "mkdocs-1.0.4.tar.gz", hash = "sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939"}, +] +more-itertools = [ + {file = "more-itertools-5.0.0.tar.gz", hash = "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4"}, + {file = "more_itertools-5.0.0-py2-none-any.whl", hash = "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc"}, + {file = "more_itertools-5.0.0-py3-none-any.whl", hash = "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"}, +] +nodeenv = [ + {file = "nodeenv-1.4.0-py2.py3-none-any.whl", hash = "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc"}, +] +packaging = [ + {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, + {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, +] +pastel = [ + {file = "pastel-0.2.0-py2.py3-none-any.whl", hash = "sha256:18b559dc3ad4ba9b8bd5baebe6503f25f36d21460f021cf27a8d889cb5d17840"}, + {file = "pastel-0.2.0.tar.gz", hash = "sha256:46155fc523bdd4efcd450bbcb3f2b94a6e3b25edc0eb493e081104ad09e1ca36"}, +] +pathlib2 = [ + {file = "pathlib2-2.3.5-py2.py3-none-any.whl", hash = "sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db"}, + {file = "pathlib2-2.3.5.tar.gz", hash = "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"}, +] +pathspec = [ + {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, + {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, +] +pep562 = [ + {file = "pep562-1.0-py2.py3-none-any.whl", hash = "sha256:d2a48b178ebf5f8dd31709cc26a19808ef794561fa2fe50ea01ea2bad4d667ef"}, + {file = "pep562-1.0.tar.gz", hash = "sha256:58cb1cc9ee63d93e62b4905a50357618d526d289919814bea1f0da8f53b79395"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +pre-commit = [ + {file = "pre_commit-1.21.0-py2.py3-none-any.whl", hash = "sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029"}, + {file = "pre_commit-1.21.0.tar.gz", hash = "sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850"}, +] +py = [ + {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, + {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, +] +pygments = [ + {file = "Pygments-2.5.2-py2.py3-none-any.whl", hash = "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b"}, + {file = "Pygments-2.5.2.tar.gz", hash = "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"}, +] +pylev = [ + {file = "pylev-1.3.0-py2.py3-none-any.whl", hash = "sha256:1d29a87beb45ebe1e821e7a3b10da2b6b2f4c79b43f482c2df1a1f748a6e114e"}, + {file = "pylev-1.3.0.tar.gz", hash = "sha256:063910098161199b81e453025653ec53556c1be7165a9b7c50be2f4d57eae1c3"}, +] +pymdown-extensions = [ + {file = "pymdown-extensions-6.2.1.tar.gz", hash = "sha256:3bbe6048275f8a0d13a0fe44e0ea201e67268aa7bb40c2544eef16abbf168f7b"}, + {file = "pymdown_extensions-6.2.1-py2.py3-none-any.whl", hash = "sha256:dce5e17b93be0572322b7d06c9a13c13a9d98694d6468277911d50ca87d26f29"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-4.6.11-py2.py3-none-any.whl", hash = "sha256:a00a7d79cbbdfa9d21e7d0298392a8dd4123316bfac545075e6f8f24c94d8c97"}, + {file = "pytest-4.6.11.tar.gz", hash = "sha256:50fa82392f2120cc3ec2ca0a75ee615be4c479e66669789771f1758332be4353"}, +] +pytest-cov = [ + {file = "pytest-cov-2.10.0.tar.gz", hash = "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87"}, + {file = "pytest_cov-2.10.0-py2.py3-none-any.whl", hash = "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, + {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, +] +pytz = [ + {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, + {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, +] +pytzdata = [ + {file = "pytzdata-2020.1-py2.py3-none-any.whl", hash = "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f"}, + {file = "pytzdata-2020.1.tar.gz", hash = "sha256:3efa13b335a00a8de1d345ae41ec78dd11c9f8807f522d39850f2dd828681540"}, +] +pyyaml = [ + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, +] +regex = [ + {file = "regex-2020.6.8-cp27-cp27m-win32.whl", hash = "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"}, + {file = "regex-2020.6.8-cp27-cp27m-win_amd64.whl", hash = "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938"}, + {file = "regex-2020.6.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89"}, + {file = "regex-2020.6.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868"}, + {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f"}, + {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded"}, + {file = "regex-2020.6.8-cp36-cp36m-win32.whl", hash = "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3"}, + {file = "regex-2020.6.8-cp36-cp36m-win_amd64.whl", hash = "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd"}, + {file = "regex-2020.6.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a"}, + {file = "regex-2020.6.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae"}, + {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a"}, + {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3"}, + {file = "regex-2020.6.8-cp37-cp37m-win32.whl", hash = "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9"}, + {file = "regex-2020.6.8-cp37-cp37m-win_amd64.whl", hash = "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29"}, + {file = "regex-2020.6.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610"}, + {file = "regex-2020.6.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387"}, + {file = "regex-2020.6.8-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910"}, + {file = "regex-2020.6.8-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf"}, + {file = "regex-2020.6.8-cp38-cp38-win32.whl", hash = "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754"}, + {file = "regex-2020.6.8-cp38-cp38-win_amd64.whl", hash = "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5"}, + {file = "regex-2020.6.8.tar.gz", hash = "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac"}, +] +scandir = [ + {file = "scandir-1.10.0-cp27-cp27m-win32.whl", hash = "sha256:92c85ac42f41ffdc35b6da57ed991575bdbe69db895507af88b9f499b701c188"}, + {file = "scandir-1.10.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cb925555f43060a1745d0a321cca94bcea927c50114b623d73179189a4e100ac"}, + {file = "scandir-1.10.0-cp34-cp34m-win32.whl", hash = "sha256:2c712840c2e2ee8dfaf36034080108d30060d759c7b73a01a52251cc8989f11f"}, + {file = "scandir-1.10.0-cp34-cp34m-win_amd64.whl", hash = "sha256:2586c94e907d99617887daed6c1d102b5ca28f1085f90446554abf1faf73123e"}, + {file = "scandir-1.10.0-cp35-cp35m-win32.whl", hash = "sha256:2b8e3888b11abb2217a32af0766bc06b65cc4a928d8727828ee68af5a967fa6f"}, + {file = "scandir-1.10.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8c5922863e44ffc00c5c693190648daa6d15e7c1207ed02d6f46a8dcc2869d32"}, + {file = "scandir-1.10.0-cp36-cp36m-win32.whl", hash = "sha256:2ae41f43797ca0c11591c0c35f2f5875fa99f8797cb1a1fd440497ec0ae4b022"}, + {file = "scandir-1.10.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7d2d7a06a252764061a020407b997dd036f7bd6a175a5ba2b345f0a357f0b3f4"}, + {file = "scandir-1.10.0-cp37-cp37m-win32.whl", hash = "sha256:67f15b6f83e6507fdc6fca22fedf6ef8b334b399ca27c6b568cbfaa82a364173"}, + {file = "scandir-1.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b24086f2375c4a094a6b51e78b4cf7ca16c721dcee2eddd7aa6494b42d6d519d"}, + {file = "scandir-1.10.0.tar.gz", hash = "sha256:4d4631f6062e658e9007ab3149a9b914f3548cb38bfb021c64f39a025ce578ae"}, +] +singledispatch = [ + {file = "singledispatch-3.4.0.3-py2.py3-none-any.whl", hash = "sha256:833b46966687b3de7f438c761ac475213e53b306740f1abfaa86e1d1aae56aa8"}, + {file = "singledispatch-3.4.0.3.tar.gz", hash = "sha256:5b06af87df13818d14f08a028e42f566640aef80805c3b50c5056b086e3c2b9c"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +toml = [ + {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, + {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, +] +tornado = [ + {file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"}, + {file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"}, + {file = "tornado-6.0.4-cp36-cp36m-win32.whl", hash = "sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673"}, + {file = "tornado-6.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a"}, + {file = "tornado-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6"}, + {file = "tornado-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b"}, + {file = "tornado-6.0.4-cp38-cp38-win32.whl", hash = "sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52"}, + {file = "tornado-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9"}, + {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, +] +tox = [ + {file = "tox-3.16.1-py2.py3-none-any.whl", hash = "sha256:60c3793f8ab194097ec75b5a9866138444f63742b0f664ec80be1222a40687c5"}, + {file = "tox-3.16.1.tar.gz", hash = "sha256:9a746cda9cadb9e1e05c7ab99f98cfcea355140d2ecac5f97520be94657c3bc7"}, +] +typed-ast = [ + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, + {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, + {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, + {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, + {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, +] +typing = [ + {file = "typing-3.7.4.1-py2-none-any.whl", hash = "sha256:c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36"}, + {file = "typing-3.7.4.1-py3-none-any.whl", hash = "sha256:f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714"}, + {file = "typing-3.7.4.1.tar.gz", hash = "sha256:91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, + {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, + {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, +] +virtualenv = [ + {file = "virtualenv-20.0.26-py2.py3-none-any.whl", hash = "sha256:c11a475400e98450403c0364eb3a2d25d42f71cf1493da64390487b666de4324"}, + {file = "virtualenv-20.0.26.tar.gz", hash = "sha256:e10cc66f40cbda459720dfe1d334c4dc15add0d80f09108224f171006a97a172"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +zipp = [ + {file = "zipp-1.2.0-py2.py3-none-any.whl", hash = "sha256:e0d9e63797e483a30d27e09fffd308c59a700d365ec34e93cc100844168bf921"}, + {file = "zipp-1.2.0.tar.gz", hash = "sha256:c70410551488251b0fee67b460fb9a536af8d6f9f008ad10ac51f615b6a521b1"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..11ac22c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,81 @@ +[tool.poetry] +name = "pendulum" +version = "2.1.2" +description = "Python datetimes made easy" +authors = ["Sébastien Eustace <sebastien@eustace.io>"] +license = "MIT" +readme = 'README.rst' +homepage = "https://pendulum.eustace.io" +repository = "https://github.com/sdispater/pendulum" +documentation = "https://pendulum.eustace.io/docs" +keywords = ['datetime', 'date', 'time'] + +packages = [ + {include = "pendulum"}, + #{include = "tests", format = "sdist"}, +] +include = [ + {path = "pendulum/py.typed"}, + # C extensions must be included in the wheel distributions + {path = "pendulum/_extensions/*.so", format = "wheel"}, + {path = "pendulum/_extensions/*.pyd", format = "wheel"}, + {path = "pendulum/parsing/*.so", format = "wheel"}, + {path = "pendulum/parsing/*.pyd", format = "wheel"}, +] + + +[tool.poetry.dependencies] +python = "~2.7 || ^3.5" +python-dateutil = "^2.6" +pytzdata = ">=2020.1" + +# typing is needed for Python < 3.5 +typing = { version = "^3.6", python = "<3.5" } + +[tool.poetry.dev-dependencies] +pytest = "^4.6" +pytest-cov = "^2.5" +pytz = ">=2018.3" +babel = "^2.5" +cleo = "^0.8.1" +tox = "^3.0" +black = { version = "^19.3b0", markers = "python_version >= '3.6' and python_version < '4.0' and implementation_name != 'pypy'" } +isort = { version = "^4.3.21", markers = "python_version >= '3.6' and python_version < '4.0'" } +pre-commit = "^1.10" +mkdocs = { version = "^1.0", python = "^3.5" } +pymdown-extensions = "^6.0" +pygments = "^2.2" +markdown-include = "^0.5.1" +freezegun = "^0.3.15" + +[tool.poetry.build] +generate-setup-file = false +script = "build.py" + +[tool.isort] +line_length = 88 +force_single_line = true +force_grid_wrap = 0 +atomic = true +include_trailing_comma = true +lines_after_imports = 2 +lines_between_types = 1 +multi_line_output = 3 +use_parentheses = true +not_skip = "__init__.py" +skip_glob = ["*/setup.py"] +filter_files = true + +known_first_party = "pendulum" +known_third_party = [ + "babel", + "cleo", + "dateutil", + "freezegun", + "pytzdata", +] + + +[build-system] +requires = ["poetry-core>=1.0.0a9"] +build-backend = "poetry.core.masonry.api" 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..4ab1b87 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,99 @@ +import pendulum +import pytest + + +@pytest.fixture(autouse=True) +def setup(): + pendulum.set_local_timezone(pendulum.timezone("America/Toronto")) + + yield + + pendulum.set_test_now() + 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, +): + 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..40a96af --- /dev/null +++ b/tests/date/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/date/test_add.py b/tests/date/test_add.py new file mode 100644 index 0000000..9c83b61 --- /dev/null +++ b/tests/date/test_add.py @@ -0,0 +1,85 @@ +from datetime import timedelta + +import pendulum +import pytest + +from ..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..1ef56c4 --- /dev/null +++ b/tests/date/test_behavior.py @@ -0,0 +1,70 @@ +import pickle + +from datetime import date + +import pendulum +import pytest + + +@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..052ae2f --- /dev/null +++ b/tests/date/test_comparison.py @@ -0,0 +1,245 @@ +from datetime import date + +import pendulum + +from ..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 not 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..615ca80 --- /dev/null +++ b/tests/date/test_construct.py @@ -0,0 +1,15 @@ +from pendulum import Date + +from ..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..62aad33 --- /dev/null +++ b/tests/date/test_day_of_week_modifiers.py @@ -0,0 +1,296 @@ +import pendulum +import pytest + +from pendulum.exceptions import PendulumException + +from ..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..29814ac --- /dev/null +++ b/tests/date/test_diff.py @@ -0,0 +1,362 @@ +from datetime import date + +import pendulum +import pytest + + +@pytest.fixture +def today(): + return pendulum.today().date() + + +def test_diff_in_years_positive(): + dt = pendulum.date(2000, 1, 1) + assert 1 == dt.diff(dt.add(years=1)).in_years() + + +def test_diff_in_years_negative_with_sign(): + dt = pendulum.date(2000, 1, 1) + assert -1 == dt.diff(dt.subtract(years=1), False).in_years() + + +def test_diff_in_years_negative_no_sign(): + dt = pendulum.date(2000, 1, 1) + assert 1 == dt.diff(dt.subtract(years=1)).in_years() + + +def test_diff_in_years_vs_default_now(today): + assert 1 == today.subtract(years=1).diff().in_years() + + +def test_diff_in_years_ensure_is_truncated(): + dt = pendulum.date(2000, 1, 1) + assert 1 == dt.diff(dt.add(years=1).add(months=7)).in_years() + + +def test_diff_in_months_positive(): + dt = pendulum.date(2000, 1, 1) + assert 13 == dt.diff(dt.add(years=1).add(months=1)).in_months() + + +def test_diff_in_months_negative_with_sign(): + dt = pendulum.date(2000, 1, 1) + + assert -11 == dt.diff(dt.subtract(years=1).add(months=1), False).in_months() + + +def test_diff_in_months_negative_no_sign(): + dt = pendulum.date(2000, 1, 1) + assert 11 == dt.diff(dt.subtract(years=1).add(months=1)).in_months() + + +def test_diff_in_months_vs_default_now(today): + assert 12 == today.subtract(years=1).diff().in_months() + + +def test_diff_in_months_ensure_is_truncated(): + dt = pendulum.date(2000, 1, 1) + assert 1 == dt.diff(dt.add(months=1).add(days=16)).in_months() + + +def test_diff_in_days_positive(): + dt = pendulum.date(2000, 1, 1) + assert 366 == dt.diff(dt.add(years=1)).in_days() + + +def test_diff_in_days_negative_with_sign(): + dt = pendulum.date(2000, 1, 1) + assert -365 == dt.diff(dt.subtract(years=1), False).in_days() + + +def test_diff_in_days_negative_no_sign(): + dt = pendulum.date(2000, 1, 1) + assert 365 == dt.diff(dt.subtract(years=1)).in_days() + + +def test_diff_in_days_vs_default_now(today): + assert 7 == today.subtract(weeks=1).diff().in_days() + + +def test_diff_in_weeks_positive(): + dt = pendulum.date(2000, 1, 1) + assert 52 == dt.diff(dt.add(years=1)).in_weeks() + + +def test_diff_in_weeks_negative_with_sign(): + dt = pendulum.date(2000, 1, 1) + assert -52 == dt.diff(dt.subtract(years=1), False).in_weeks() + + +def test_diff_in_weeks_negative_no_sign(): + dt = pendulum.date(2000, 1, 1) + assert 52 == dt.diff(dt.subtract(years=1)).in_weeks() + + +def test_diff_in_weeks_vs_default_now(today): + assert 1 == today.subtract(weeks=1).diff().in_weeks() + + +def test_diff_in_weeks_ensure_is_truncated(): + dt = pendulum.date(2000, 1, 1) + assert 0 == dt.diff(dt.add(weeks=1).subtract(days=1)).in_weeks() + + +def test_diff_for_humans_now_and_day(today): + assert "1 day ago" == today.subtract(days=1).diff_for_humans() + + +def test_diff_for_humans_now_and_days(today): + assert "2 days ago" == today.subtract(days=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_week(today): + assert "6 days ago" == today.subtract(days=6).diff_for_humans() + + +def test_diff_for_humans_now_and_week(today): + assert "1 week ago" == today.subtract(weeks=1).diff_for_humans() + + +def test_diff_for_humans_now_and_weeks(today): + assert "2 weeks ago" == today.subtract(weeks=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_month(today): + assert "3 weeks ago" == today.subtract(weeks=3).diff_for_humans() + + +def test_diff_for_humans_now_and_month(): + with pendulum.test(pendulum.datetime(2016, 3, 1)): + today = pendulum.today().date() + + assert "4 weeks ago" == today.subtract(weeks=4).diff_for_humans() + assert "1 month ago" == today.subtract(months=1).diff_for_humans() + + with pendulum.test(pendulum.datetime(2017, 2, 28)): + today = pendulum.today().date() + + assert "1 month ago" == today.subtract(weeks=4).diff_for_humans() + + +def test_diff_for_humans_now_and_months(today): + assert "2 months ago" == today.subtract(months=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_year(today): + assert "11 months ago" == today.subtract(months=11).diff_for_humans() + + +def test_diff_for_humans_now_and_year(today): + assert "1 year ago" == today.subtract(years=1).diff_for_humans() + + +def test_diff_for_humans_now_and_years(today): + assert "2 years ago" == today.subtract(years=2).diff_for_humans() + + +def test_diff_for_humans_now_and_future_day(today): + assert "in 1 day" == today.add(days=1).diff_for_humans() + + +def test_diff_for_humans_now_and_future_days(today): + assert "in 2 days" == today.add(days=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_future_week(today): + assert "in 6 days" == today.add(days=6).diff_for_humans() + + +def test_diff_for_humans_now_and_future_week(today): + assert "in 1 week" == today.add(weeks=1).diff_for_humans() + + +def test_diff_for_humans_now_and_future_weeks(today): + assert "in 2 weeks" == today.add(weeks=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_future_month(today): + assert "in 3 weeks" == today.add(weeks=3).diff_for_humans() + + +def test_diff_for_humans_now_and_future_month(): + with pendulum.test(pendulum.datetime(2016, 3, 1)): + today = pendulum.today().date() + + assert "in 4 weeks" == today.add(weeks=4).diff_for_humans() + assert "in 1 month" == today.add(months=1).diff_for_humans() + + with pendulum.test(pendulum.datetime(2017, 3, 31)): + today = pendulum.today().date() + + assert "in 1 month" == today.add(months=1).diff_for_humans() + + with pendulum.test(pendulum.datetime(2017, 4, 30)): + today = pendulum.today().date() + + assert "in 1 month" == today.add(months=1).diff_for_humans() + + with pendulum.test(pendulum.datetime(2017, 1, 31)): + today = pendulum.today().date() + + assert "in 1 month" == today.add(weeks=4).diff_for_humans() + + +def test_diff_for_humans_now_and_future_months(today): + assert "in 2 months" == today.add(months=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_future_year(today): + assert "in 11 months" == today.add(months=11).diff_for_humans() + + +def test_diff_for_humans_now_and_future_year(today): + assert "in 1 year" == today.add(years=1).diff_for_humans() + + +def test_diff_for_humans_now_and_future_years(today): + assert "in 2 years" == today.add(years=2).diff_for_humans() + + +def test_diff_for_humans_other_and_day(today): + assert "1 day before" == today.diff_for_humans(today.add(days=1)) + + +def test_diff_for_humans_other_and_days(today): + assert "2 days before" == today.diff_for_humans(today.add(days=2)) + + +def test_diff_for_humans_other_and_nearly_week(today): + assert "6 days before" == today.diff_for_humans(today.add(days=6)) + + +def test_diff_for_humans_other_and_week(today): + assert "1 week before" == today.diff_for_humans(today.add(weeks=1)) + + +def test_diff_for_humans_other_and_weeks(today): + assert "2 weeks before" == today.diff_for_humans(today.add(weeks=2)) + + +def test_diff_for_humans_other_and_nearly_month(today): + assert "3 weeks before" == today.diff_for_humans(today.add(weeks=3)) + + +def test_diff_for_humans_other_and_month(): + with pendulum.test(pendulum.datetime(2016, 3, 1)): + today = pendulum.today().date() + + assert "4 weeks before" == today.diff_for_humans(today.add(weeks=4)) + assert "1 month before" == today.diff_for_humans(today.add(months=1)) + + with pendulum.test(pendulum.datetime(2017, 3, 31)): + today = pendulum.today().date() + + assert "1 month before" == today.diff_for_humans(today.add(months=1)) + + with pendulum.test(pendulum.datetime(2017, 4, 30)): + today = pendulum.today().date() + + assert "1 month before" == today.diff_for_humans(today.add(months=1)) + + with pendulum.test(pendulum.datetime(2017, 1, 31)): + today = pendulum.today().date() + + assert "1 month before" == today.diff_for_humans(today.add(weeks=4)) + + +def test_diff_for_humans_other_and_months(today): + assert "2 months before" == today.diff_for_humans(today.add(months=2)) + + +def test_diff_for_humans_other_and_nearly_year(today): + assert "11 months before" == today.diff_for_humans(today.add(months=11)) + + +def test_diff_for_humans_other_and_year(today): + assert "1 year before" == today.diff_for_humans(today.add(years=1)) + + +def test_diff_for_humans_other_and_years(today): + assert "2 years before" == today.diff_for_humans(today.add(years=2)) + + +def test_diff_for_humans_other_and_future_day(today): + assert "1 day after" == today.diff_for_humans(today.subtract(days=1)) + + +def test_diff_for_humans_other_and_future_days(today): + assert "2 days after" == today.diff_for_humans(today.subtract(days=2)) + + +def test_diff_for_humans_other_and_nearly_future_week(today): + assert "6 days after" == today.diff_for_humans(today.subtract(days=6)) + + +def test_diff_for_humans_other_and_future_week(today): + assert "1 week after" == today.diff_for_humans(today.subtract(weeks=1)) + + +def test_diff_for_humans_other_and_future_weeks(today): + assert "2 weeks after" == today.diff_for_humans(today.subtract(weeks=2)) + + +def test_diff_for_humans_other_and_nearly_future_month(today): + assert "3 weeks after" == today.diff_for_humans(today.subtract(weeks=3)) + + +def test_diff_for_humans_other_and_future_month(): + with pendulum.test(pendulum.datetime(2016, 3, 1)): + today = pendulum.today().date() + + assert "4 weeks after" == today.diff_for_humans(today.subtract(weeks=4)) + assert "1 month after" == today.diff_for_humans(today.subtract(months=1)) + + with pendulum.test(pendulum.datetime(2017, 2, 28)): + today = pendulum.today().date() + + assert "1 month after" == today.diff_for_humans(today.subtract(weeks=4)) + + +def test_diff_for_humans_other_and_future_months(today): + assert "2 months after" == today.diff_for_humans(today.subtract(months=2)) + + +def test_diff_for_humans_other_and_nearly_future_year(today): + assert "11 months after" == today.diff_for_humans(today.subtract(months=11)) + + +def test_diff_for_humans_other_and_future_year(today): + assert "1 year after" == today.diff_for_humans(today.subtract(years=1)) + + +def test_diff_for_humans_other_and_future_years(today): + assert "2 years after" == today.diff_for_humans(today.subtract(years=2)) + + +def test_diff_for_humans_absolute_days(today): + assert "2 days" == today.diff_for_humans(today.subtract(days=2), True) + assert "2 days" == today.diff_for_humans(today.add(days=2), True) + + +def test_diff_for_humans_absolute_weeks(today): + assert "2 weeks" == today.diff_for_humans(today.subtract(weeks=2), True) + assert "2 weeks" == today.diff_for_humans(today.add(weeks=2), True) + + +def test_diff_for_humans_absolute_months(today): + assert "2 months" == today.diff_for_humans(today.subtract(months=2), True) + assert "2 months" == today.diff_for_humans(today.add(months=2), True) + + +def test_diff_for_humans_absolute_years(today): + assert "1 year" == today.diff_for_humans(today.subtract(years=1), True) + assert "1 year" == today.diff_for_humans(today.add(years=1), True) + + +def test_subtraction(): + d = pendulum.date(2016, 7, 5) + future_dt = date(2016, 7, 6) + future = d.add(days=1) + + assert 86400 == (future - d).total_seconds() + assert 86400 == (future_dt - d).total_seconds() diff --git a/tests/date/test_fluent_setters.py b/tests/date/test_fluent_setters.py new file mode 100644 index 0000000..4eaea8c --- /dev/null +++ b/tests/date/test_fluent_setters.py @@ -0,0 +1,27 @@ +import pendulum + +from ..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..3d95ae3 --- /dev/null +++ b/tests/date/test_getters.py @@ -0,0 +1,85 @@ +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..8a82540 --- /dev/null +++ b/tests/date/test_start_end_of.py @@ -0,0 +1,250 @@ +import pendulum +import pytest + +from pendulum import Date + +from ..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..2323bc3 --- /dev/null +++ b/tests/date/test_strings.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +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 = u"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 "{}".format(d) == "1975-12-25" + assert "{:YYYY}".format(d) == "1975" + assert "{:%Y}".format(d) == "1975" diff --git a/tests/date/test_sub.py b/tests/date/test_sub.py new file mode 100644 index 0000000..dcf7a2a --- /dev/null +++ b/tests/date/test_sub.py @@ -0,0 +1,87 @@ +from datetime import datetime +from datetime import timedelta + +import pendulum +import pytest + +from ..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..40a96af --- /dev/null +++ b/tests/datetime/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/datetime/test_add.py b/tests/datetime/test_add.py new file mode 100644 index 0000000..bb338ba --- /dev/null +++ b/tests/datetime/test_add.py @@ -0,0 +1,267 @@ +from datetime import timedelta + +import pendulum +import pytest + +from ..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 2017 == d.year + assert 6 == d.month + assert 20 == d.day + assert 3 == d.hour + assert 13 == d.minute + assert 0 == d.second + assert 777777 == d.microsecond + + +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") + print(dt) + dt = dt.add(hours=1) + print(dt) + + 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..04c0b1e --- /dev/null +++ b/tests/datetime/test_behavior.py @@ -0,0 +1,168 @@ +import pickle + +from copy import deepcopy +from datetime import date +from datetime import datetime +from datetime import time +from datetime import timedelta + +import pendulum +import pytest + +from pendulum import timezone +from pendulum.tz.timezone import Timezone + + +@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") + + assert dt.dst() == timedelta(0, 6000) + + +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..0819f80 --- /dev/null +++ b/tests/datetime/test_comparison.py @@ -0,0 +1,391 @@ +from datetime import datetime + +import pendulum +import pytz + +from ..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.test(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.test(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..2e45ead --- /dev/null +++ b/tests/datetime/test_construct.py @@ -0,0 +1,181 @@ +import os + +from datetime import datetime + +from dateutil import tz +from freezegun import freeze_time + +import pendulum +import pytest +import pytz + +from pendulum import DateTime +from pendulum.tz import timezone +from pendulum.utils._compat import PY36 + +from ..conftest import assert_datetime + + +@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 + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +@freeze_time("2016-03-27 00:30:00") +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() + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +@freeze_time("2016-03-27 01:30:00") +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() + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +@freeze_time("2016-10-30 00:30:00") +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() + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +@freeze_time("2016-10-30 01:30:00") +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 "+06:00" == now.timezone_name + + +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..e7e24d7 --- /dev/null +++ b/tests/datetime/test_create_from_timestamp.py @@ -0,0 +1,23 @@ +import pendulum + +from pendulum import timezone + +from ..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..1526f4a --- /dev/null +++ b/tests/datetime/test_day_of_week_modifiers.py @@ -0,0 +1,312 @@ +import pendulum +import pytest + +from pendulum.exceptions import PendulumException + +from ..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..b936b90 --- /dev/null +++ b/tests/datetime/test_diff.py @@ -0,0 +1,823 @@ +from datetime import datetime + +import pendulum +import pytest + + +def test_diff_in_years_positive(): + dt = pendulum.datetime(2000, 1, 1) + assert 1 == dt.diff(dt.add(years=1)).in_years() + + +def test_diff_in_years_negative_with_sign(): + dt = pendulum.datetime(2000, 1, 1) + assert -1 == dt.diff(dt.subtract(years=1), False).in_years() + + +def test_diff_in_years_negative_no_sign(): + dt = pendulum.datetime(2000, 1, 1) + assert 1 == dt.diff(dt.subtract(years=1)).in_years() + + +def test_diff_in_years_vs_default_now(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert 1 == pendulum.now().subtract(years=1).diff().in_years() + + +def test_diff_in_years_ensure_is_truncated(): + dt = pendulum.datetime(2000, 1, 1) + assert 1 == dt.diff(dt.add(years=1).add(months=7)).in_years() + + +def test_diff_in_months_positive(): + dt = pendulum.datetime(2000, 1, 1) + assert 13 == dt.diff(dt.add(years=1).add(months=1)).in_months() + + +def test_diff_in_months_negative_with_sign(): + dt = pendulum.datetime(2000, 1, 1) + assert -11 == dt.diff(dt.subtract(years=1).add(months=1), False).in_months() + + +def test_diff_in_months_negative_no_sign(): + dt = pendulum.datetime(2000, 1, 1) + assert 11 == dt.diff(dt.subtract(years=1).add(months=1)).in_months() + + +def test_diff_in_months_vs_default_now(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert 12 == pendulum.now().subtract(years=1).diff().in_months() + + +def test_diff_in_months_ensure_is_truncated(): + dt = pendulum.datetime(2000, 1, 1) + assert 1 == dt.diff(dt.add(months=1).add(days=16)).in_months() + + +def test_diff_in_days_positive(): + dt = pendulum.datetime(2000, 1, 1) + assert 366 == dt.diff(dt.add(years=1)).in_days() + + +def test_diff_in_days_negative_with_sign(): + dt = pendulum.datetime(2000, 1, 1) + assert -365 == dt.diff(dt.subtract(years=1), False).in_days() + + +def test_diff_in_days_negative_no_sign(): + dt = pendulum.datetime(2000, 1, 1) + assert 365 == dt.diff(dt.subtract(years=1)).in_days() + + +def test_diff_in_days_vs_default_now(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert 7 == pendulum.now().subtract(weeks=1).diff().in_days() + + +def test_diff_in_days_ensure_is_truncated(): + dt = pendulum.datetime(2000, 1, 1) + assert 1 == dt.diff(dt.add(days=1).add(hours=13)).in_days() + + +def test_diff_in_weeks_positive(): + dt = pendulum.datetime(2000, 1, 1) + assert 52 == dt.diff(dt.add(years=1)).in_weeks() + + +def test_diff_in_weeks_negative_with_sign(): + dt = pendulum.datetime(2000, 1, 1) + assert -52 == dt.diff(dt.subtract(years=1), False).in_weeks() + + +def test_diff_in_weeks_negative_no_sign(): + dt = pendulum.datetime(2000, 1, 1) + assert 52 == dt.diff(dt.subtract(years=1)).in_weeks() + + +def test_diff_in_weeks_vs_default_now(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert 1 == pendulum.now().subtract(weeks=1).diff().in_weeks() + + +def test_diff_in_weeks_ensure_is_truncated(): + dt = pendulum.datetime(2000, 1, 1) + assert 0 == dt.diff(dt.add(weeks=1).subtract(days=1)).in_weeks() + + +def test_diff_in_hours_positive(): + dt = pendulum.datetime(2000, 1, 1) + assert 26 == dt.diff(dt.add(days=1).add(hours=2)).in_hours() + + +def test_diff_in_hours_negative_with_sign(): + dt = pendulum.datetime(2000, 1, 1) + assert -22 == dt.diff(dt.subtract(days=1).add(hours=2), False).in_hours() + + +def test_diff_in_hours_negative_no_sign(): + dt = pendulum.datetime(2000, 1, 1) + assert 22 == dt.diff(dt.subtract(days=1).add(hours=2)).in_hours() + + +def test_diff_in_hours_vs_default_now(): + with pendulum.test(pendulum.datetime(2012, 1, 15)): + assert 48 == pendulum.now().subtract(days=2).diff().in_hours() + + +def test_diff_in_hours_ensure_is_truncated(): + dt = pendulum.datetime(2000, 1, 1) + assert 1 == dt.diff(dt.add(hours=1).add(minutes=31)).in_hours() + + +def test_diff_in_minutes_positive(): + dt = pendulum.datetime(2000, 1, 1) + assert 62 == dt.diff(dt.add(hours=1).add(minutes=2)).in_minutes() + + +def test_diff_in_minutes_positive_big(): + dt = pendulum.datetime(2000, 1, 1) + assert 1502 == dt.diff(dt.add(hours=25).add(minutes=2)).in_minutes() + + +def test_diff_in_minutes_negative_with_sign(): + dt = pendulum.datetime(2000, 1, 1) + assert -58 == dt.diff(dt.subtract(hours=1).add(minutes=2), False).in_minutes() + + +def test_diff_in_minutes_negative_no_sign(): + dt = pendulum.datetime(2000, 1, 1) + assert 58 == dt.diff(dt.subtract(hours=1).add(minutes=2)).in_minutes() + + +def test_diff_in_minutes_vs_default_now(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert 60 == pendulum.now().subtract(hours=1).diff().in_minutes() + + +def test_diff_in_minutes_ensure_is_truncated(): + dt = pendulum.datetime(2000, 1, 1) + assert 1 == dt.diff(dt.add(minutes=1).add(seconds=59)).in_minutes() + + +def test_diff_in_seconds_positive(): + dt = pendulum.datetime(2000, 1, 1) + assert 62 == dt.diff(dt.add(minutes=1).add(seconds=2)).in_seconds() + + +def test_diff_in_seconds_positive_big(): + dt = pendulum.datetime(2000, 1, 1) + assert 7202 == dt.diff(dt.add(hours=2).add(seconds=2)).in_seconds() + + +def test_diff_in_seconds_negative_with_sign(): + dt = pendulum.datetime(2000, 1, 1) + assert -58 == dt.diff(dt.subtract(minutes=1).add(seconds=2), False).in_seconds() + + +def test_diff_in_seconds_negative_no_sign(): + dt = pendulum.datetime(2000, 1, 1) + assert 58 == dt.diff(dt.subtract(minutes=1).add(seconds=2)).in_seconds() + + +def test_diff_in_seconds_vs_default_now(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert 3600 == pendulum.now().subtract(hours=1).diff().in_seconds() + + +def test_diff_in_seconds_ensure_is_truncated(): + dt = pendulum.datetime(2000, 1, 1) + assert 1 == dt.diff(dt.add(seconds=1.9)).in_seconds() + + +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 3 * 60 * 60 == dt_ottawa.diff(dt_vancouver).in_seconds() + + +def test_diff_for_humans_now_and_second(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "a few seconds ago" == pendulum.now().diff_for_humans() + + +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.test(here_now): + assert "a few seconds ago" == here_now.diff_for_humans() + + +def test_diff_for_humans_now_and_seconds(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert ( + "a few seconds ago" == pendulum.now().subtract(seconds=2).diff_for_humans() + ) + + +def test_diff_for_humans_now_and_nearly_minute(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "59 seconds ago" == pendulum.now().subtract(seconds=59).diff_for_humans() + + +def test_diff_for_humans_now_and_minute(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "1 minute ago" == pendulum.now().subtract(minutes=1).diff_for_humans() + + +def test_diff_for_humans_now_and_minutes(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 minutes ago" == pendulum.now().subtract(minutes=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_hour(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "59 minutes ago" == pendulum.now().subtract(minutes=59).diff_for_humans() + + +def test_diff_for_humans_now_and_hour(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "1 hour ago" == pendulum.now().subtract(hours=1).diff_for_humans() + + +def test_diff_for_humans_now_and_hours(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 hours ago" == pendulum.now().subtract(hours=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_day(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "23 hours ago" == pendulum.now().subtract(hours=23).diff_for_humans() + + +def test_diff_for_humans_now_and_day(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "1 day ago" == pendulum.now().subtract(days=1).diff_for_humans() + + +def test_diff_for_humans_now_and_days(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 days ago" == pendulum.now().subtract(days=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_week(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "6 days ago" == pendulum.now().subtract(days=6).diff_for_humans() + + +def test_diff_for_humans_now_and_week(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "1 week ago" == pendulum.now().subtract(weeks=1).diff_for_humans() + + +def test_diff_for_humans_now_and_weeks(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 weeks ago" == pendulum.now().subtract(weeks=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_month(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "3 weeks ago" == pendulum.now().subtract(weeks=3).diff_for_humans() + + +def test_diff_for_humans_now_and_month(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "4 weeks ago" == pendulum.now().subtract(weeks=4).diff_for_humans() + assert "1 month ago" == pendulum.now().subtract(months=1).diff_for_humans() + + +def test_diff_for_humans_now_and_months(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 months ago" == pendulum.now().subtract(months=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_year(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "11 months ago" == pendulum.now().subtract(months=11).diff_for_humans() + + +def test_diff_for_humans_now_and_year(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "1 year ago" == pendulum.now().subtract(years=1).diff_for_humans() + + +def test_diff_for_humans_now_and_years(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 years ago" == pendulum.now().subtract(years=2).diff_for_humans() + + +def test_diff_for_humans_now_and_future_second(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in a few seconds" == pendulum.now().add(seconds=1).diff_for_humans() + + +def test_diff_for_humans_now_and_future_seconds(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in a few seconds" == pendulum.now().add(seconds=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_future_minute(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 59 seconds" == pendulum.now().add(seconds=59).diff_for_humans() + + +def test_diff_for_humans_now_and_future_minute(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 1 minute" == pendulum.now().add(minutes=1).diff_for_humans() + + +def test_diff_for_humans_now_and_future_minutes(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 2 minutes" == pendulum.now().add(minutes=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_future_hour(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 59 minutes" == pendulum.now().add(minutes=59).diff_for_humans() + + +def test_diff_for_humans_now_and_future_hour(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 1 hour" == pendulum.now().add(hours=1).diff_for_humans() + + +def test_diff_for_humans_now_and_future_hours(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 2 hours" == pendulum.now().add(hours=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_future_day(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 23 hours" == pendulum.now().add(hours=23).diff_for_humans() + + +def test_diff_for_humans_now_and_future_day(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 1 day" == pendulum.now().add(days=1).diff_for_humans() + + +def test_diff_for_humans_now_and_future_days(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 2 days" == pendulum.now().add(days=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_future_week(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 6 days" == pendulum.now().add(days=6).diff_for_humans() + + +def test_diff_for_humans_now_and_future_week(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 1 week" == pendulum.now().add(weeks=1).diff_for_humans() + + +def test_diff_for_humans_now_and_future_weeks(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 2 weeks" == pendulum.now().add(weeks=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_future_month(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 3 weeks" == pendulum.now().add(weeks=3).diff_for_humans() + + +def test_diff_for_humans_now_and_future_month(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 4 weeks" == pendulum.now().add(weeks=4).diff_for_humans() + assert "in 1 month" == pendulum.now().add(months=1).diff_for_humans() + + +def test_diff_for_humans_now_and_future_months(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 2 months" == pendulum.now().add(months=2).diff_for_humans() + + +def test_diff_for_humans_now_and_nearly_future_year(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 11 months" == pendulum.now().add(months=11).diff_for_humans() + + +def test_diff_for_humans_now_and_future_year(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 1 year" == pendulum.now().add(years=1).diff_for_humans() + + +def test_diff_for_humans_now_and_future_years(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "in 2 years" == pendulum.now().add(years=2).diff_for_humans() + + +def test_diff_for_humans_other_and_second(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "a few seconds before" == pendulum.now().diff_for_humans( + pendulum.now().add(seconds=1) + ) + + +def test_diff_for_humans_other_and_seconds(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "a few seconds before" == pendulum.now().diff_for_humans( + pendulum.now().add(seconds=2) + ) + + +def test_diff_for_humans_other_and_nearly_minute(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "59 seconds before" == pendulum.now().diff_for_humans( + pendulum.now().add(seconds=59) + ) + + +def test_diff_for_humans_other_and_minute(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "1 minute before" == pendulum.now().diff_for_humans( + pendulum.now().add(minutes=1) + ) + + +def test_diff_for_humans_other_and_minutes(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 minutes before" == pendulum.now().diff_for_humans( + pendulum.now().add(minutes=2) + ) + + +def test_diff_for_humans_other_and_nearly_hour(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "59 minutes before" == pendulum.now().diff_for_humans( + pendulum.now().add(minutes=59) + ) + + +def test_diff_for_humans_other_and_hour(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "1 hour before" == pendulum.now().diff_for_humans( + pendulum.now().add(hours=1) + ) + + +def test_diff_for_humans_other_and_hours(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 hours before" == pendulum.now().diff_for_humans( + pendulum.now().add(hours=2) + ) + + +def test_diff_for_humans_other_and_nearly_day(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "23 hours before" == pendulum.now().diff_for_humans( + pendulum.now().add(hours=23) + ) + + +def test_diff_for_humans_other_and_day(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "1 day before" == pendulum.now().diff_for_humans( + pendulum.now().add(days=1) + ) + + +def test_diff_for_humans_other_and_days(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 days before" == pendulum.now().diff_for_humans( + pendulum.now().add(days=2) + ) + + +def test_diff_for_humans_other_and_nearly_week(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "6 days before" == pendulum.now().diff_for_humans( + pendulum.now().add(days=6) + ) + + +def test_diff_for_humans_other_and_week(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "1 week before" == pendulum.now().diff_for_humans( + pendulum.now().add(weeks=1) + ) + + +def test_diff_for_humans_other_and_weeks(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 weeks before" == pendulum.now().diff_for_humans( + pendulum.now().add(weeks=2) + ) + + +def test_diff_for_humans_other_and_nearly_month(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "3 weeks before" == pendulum.now().diff_for_humans( + pendulum.now().add(weeks=3) + ) + + +def test_diff_for_humans_other_and_month(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "4 weeks before" == pendulum.now().diff_for_humans( + pendulum.now().add(weeks=4) + ) + assert "1 month before" == pendulum.now().diff_for_humans( + pendulum.now().add(months=1) + ) + + +def test_diff_for_humans_other_and_months(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 months before" == pendulum.now().diff_for_humans( + pendulum.now().add(months=2) + ) + + +def test_diff_for_humans_other_and_nearly_year(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "11 months before" == pendulum.now().diff_for_humans( + pendulum.now().add(months=11) + ) + + +def test_diff_for_humans_other_and_year(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "1 year before" == pendulum.now().diff_for_humans( + pendulum.now().add(years=1) + ) + + +def test_diff_for_humans_other_and_years(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 years before" == pendulum.now().diff_for_humans( + pendulum.now().add(years=2) + ) + + +def test_diff_for_humans_other_and_future_second(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "a few seconds after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(seconds=1) + ) + + +def test_diff_for_humans_other_and_future_seconds(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "a few seconds after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(seconds=2) + ) + + +def test_diff_for_humans_other_and_nearly_future_minute(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "59 seconds after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(seconds=59) + ) + + +def test_diff_for_humans_other_and_future_minute(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "1 minute after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(minutes=1) + ) + + +def test_diff_for_humans_other_and_future_minutes(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 minutes after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(minutes=2) + ) + + +def test_diff_for_humans_other_and_nearly_future_hour(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "59 minutes after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(minutes=59) + ) + + +def test_diff_for_humans_other_and_future_hour(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "1 hour after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(hours=1) + ) + + +def test_diff_for_humans_other_and_future_hours(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 hours after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(hours=2) + ) + + +def test_diff_for_humans_other_and_nearly_future_day(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "23 hours after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(hours=23) + ) + + +def test_diff_for_humans_other_and_future_day(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "1 day after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(days=1) + ) + + +def test_diff_for_humans_other_and_future_days(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 days after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(days=2) + ) + + +def test_diff_for_humans_other_and_nearly_future_week(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "6 days after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(days=6) + ) + + +def test_diff_for_humans_other_and_future_week(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "1 week after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(weeks=1) + ) + + +def test_diff_for_humans_other_and_future_weeks(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 weeks after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(weeks=2) + ) + + +def test_diff_for_humans_other_and_nearly_future_month(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "3 weeks after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(weeks=3) + ) + + +def test_diff_for_humans_other_and_future_month(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "4 weeks after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(weeks=4) + ) + assert "1 month after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(months=1) + ) + + +def test_diff_for_humans_other_and_future_months(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 months after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(months=2) + ) + + +def test_diff_for_humans_other_and_nearly_future_year(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "11 months after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(months=11) + ) + + +def test_diff_for_humans_other_and_future_year(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "1 year after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(years=1) + ) + + +def test_diff_for_humans_other_and_future_years(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 years after" == pendulum.now().diff_for_humans( + pendulum.now().subtract(years=2) + ) + + +def test_diff_for_humans_absolute_seconds(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "59 seconds" == pendulum.now().diff_for_humans( + pendulum.now().subtract(seconds=59), True + ) + assert "59 seconds" == pendulum.now().diff_for_humans( + pendulum.now().add(seconds=59), True + ) + + +def test_diff_for_humans_absolute_minutes(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "30 minutes" == pendulum.now().diff_for_humans( + pendulum.now().subtract(minutes=30), True + ) + assert "30 minutes" == pendulum.now().diff_for_humans( + pendulum.now().add(minutes=30), True + ) + + +def test_diff_for_humans_absolute_hours(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "3 hours" == pendulum.now().diff_for_humans( + pendulum.now().subtract(hours=3), True + ) + assert "3 hours" == pendulum.now().diff_for_humans( + pendulum.now().add(hours=3), True + ) + + +def test_diff_for_humans_absolute_days(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 days" == pendulum.now().diff_for_humans( + pendulum.now().subtract(days=2), True + ) + assert "2 days" == pendulum.now().diff_for_humans( + pendulum.now().add(days=2), True + ) + + +def test_diff_for_humans_absolute_weeks(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 weeks" == pendulum.now().diff_for_humans( + pendulum.now().subtract(weeks=2), True + ) + assert "2 weeks" == pendulum.now().diff_for_humans( + pendulum.now().add(weeks=2), True + ) + + +def test_diff_for_humans_absolute_months(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "2 months" == pendulum.now().diff_for_humans( + pendulum.now().subtract(months=2), True + ) + assert "2 months" == pendulum.now().diff_for_humans( + pendulum.now().add(months=2), True + ) + + +def test_diff_for_humans_absolute_years(): + with pendulum.test(pendulum.datetime(2012, 1, 1, 1, 2, 3)): + assert "1 year" == pendulum.now().diff_for_humans( + pendulum.now().subtract(years=1), True + ) + assert "1 year" == pendulum.now().diff_for_humans( + pendulum.now().add(years=1), True + ) + + +def test_diff_for_humans_accuracy(): + now = pendulum.now("utc") + + with pendulum.test(now.add(microseconds=200)): + assert "1 year" == now.add(years=1).diff_for_humans(absolute=True) + assert "11 months" == now.add(months=11).diff_for_humans(absolute=True) + assert "4 weeks" == now.add(days=27).diff_for_humans(absolute=True) + assert "1 year" == now.add(years=1, months=3).diff_for_humans(absolute=True) + assert "2 years" == now.add(years=1, months=8).diff_for_humans(absolute=True) + + # DST + now = pendulum.datetime(2017, 3, 7, tz="America/Toronto") + with pendulum.test(now): + assert "6 days" == now.add(days=6).diff_for_humans(absolute=True) + + +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 3600 == (future - d).total_seconds() + assert 3600 == (future_dt - d).total_seconds() + + +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", + dst_rule=pendulum.PRE_TRANSITION, + ) + 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..13a313f --- /dev/null +++ b/tests/datetime/test_fluent_setters.py @@ -0,0 +1,179 @@ +from datetime import datetime + +import pendulum + +from ..conftest import assert_datetime + + +def test_fluid_year_setter(): + d = pendulum.now() + new = d.set(year=1995) + assert isinstance(new, datetime) + assert 1995 == new.year + 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 11 == new.month + assert 7 == d.month + + +def test_fluid_day_setter(): + d = pendulum.datetime(2016, 7, 2, 0, 41, 20) + new = d.set(day=9) + assert isinstance(new, datetime) + assert 9 == new.day + assert 2 == d.day + + +def test_fluid_hour_setter(): + d = pendulum.datetime(2016, 7, 2, 0, 41, 20) + new = d.set(hour=5) + assert isinstance(new, datetime) + assert 5 == new.hour + assert 0 == d.hour + + +def test_fluid_minute_setter(): + d = pendulum.datetime(2016, 7, 2, 0, 41, 20) + new = d.set(minute=32) + assert isinstance(new, datetime) + assert 32 == new.minute + assert 41 == d.minute + + +def test_fluid_second_setter(): + d = pendulum.datetime(2016, 7, 2, 0, 41, 20) + new = d.set(second=49) + assert isinstance(new, datetime) + assert 49 == new.second + assert 20 == d.second + + +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 987654 == new.microsecond + assert 123456 == d.microsecond + + +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 "Europe/Paris" == new.timezone_name + assert "Europe/Paris" == new.tzinfo.name + + +def test_fluid_on(): + d = pendulum.datetime(2016, 7, 2, 0, 41, 20) + new = d.on(1995, 11, 9) + assert isinstance(new, datetime) + assert 1995 == new.year + assert 11 == new.month + assert 9 == new.day + assert 2016 == d.year + assert 7 == d.month + assert 2 == d.day + + +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 2013 == new.year + assert 4 == new.month + assert 1 == new.day + assert 7200 == new.offset + assert 2013 == d.year + assert 3 == d.month + assert 31 == d.day + assert 3600 == d.offset + + +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 5 == new.hour + assert 32 == new.minute + assert 49 == new.second + assert 0 == d.microsecond + assert 0 == d.hour + assert 41 == d.minute + assert 20 == d.second + assert 123456 == new.microsecond + + +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 3 == new.hour + assert 30 == new.minute + assert 0 == new.second + + +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..0949332 --- /dev/null +++ b/tests/datetime/test_from_format.py @@ -0,0 +1,207 @@ +import pendulum +import pytest + +from pendulum.utils._compat import PY2 + +from ..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 "UTC" == d.timezone_name + + +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 "Europe/London" == d.timezone_name + + +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 "Europe/London" == d.timezone_name + + +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 "+00:00" == d.timezone_name + + +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 "UTC" == d.timezone_name + + +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) + + # Python 2.7 loses precision for x timestamps + # so we don't test + if fmt == "x" and PY2: + return + + with pendulum.test(now): + 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.test(now): + 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.test(now): + with 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 "UTC" == d.timezone_name diff --git a/tests/datetime/test_getters.py b/tests/datetime/test_getters.py new file mode 100644 index 0000000..264f4da --- /dev/null +++ b/tests/datetime/test_getters.py @@ -0,0 +1,260 @@ +import struct + +import pendulum +import pytest + +from pendulum import DateTime +from pendulum.tz import timezone +from pendulum.utils._compat import _HAS_FOLD + +from ..conftest import assert_date +from ..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", dst_rule=pendulum.PRE_TRANSITION + ) + d_post = pendulum.datetime( + 2012, 10, 28, 2, 0, tz="Europe/Warsaw", dst_rule=pendulum.POST_TRANSITION + ) + + if _HAS_FOLD: + # 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 + else: + # when the transition is not recognizable + # then the difference should be equal to zero hours + assert d_post.timestamp() - d_pre.timestamp() == 0 + assert d_post.float_timestamp - d_pre.float_timestamp == 0 + assert d_post.int_timestamp - d_pre.int_timestamp == 0 + + +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.test(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.test(DateTime(2000, 1, 1)): + 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..61afaae --- /dev/null +++ b/tests/datetime/test_naive.py @@ -0,0 +1,76 @@ +import pendulum + +from ..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..11466f4 --- /dev/null +++ b/tests/datetime/test_replace.py @@ -0,0 +1,59 @@ +import pendulum + +from ..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..1904c78 --- /dev/null +++ b/tests/datetime/test_start_end_of.py @@ -0,0 +1,282 @@ +import pendulum +import pytest + +from ..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..7583014 --- /dev/null +++ b/tests/datetime/test_strings.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +import pendulum +import pytest + + +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 "1975-12-25" == d.to_date_string() + + +def test_to_formatted_date_string(): + d = pendulum.datetime(1975, 12, 25, 14, 15, 16) + + assert "Dec 25, 1975" == d.to_formatted_date_string() + + +def test_to_timestring(): + d = pendulum.datetime(1975, 12, 25, 14, 15, 16) + + assert "14:15:16" == d.to_time_string() + + +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 = "DateTime(1975, 12, 25, 14, 15, 16, tzinfo={})".format(repr(d.tzinfo)) + assert repr(d) == expected + + d = pendulum.datetime(1975, 12, 25, 14, 15, 16, 123456, tz="local") + expected = "DateTime(1975, 12, 25, 14, 15, 16, 123456, tzinfo={})".format( + repr(d.tzinfo) + ) + assert repr(d) == expected + + +def test_format_with_locale(): + d = pendulum.datetime(1975, 12, 25, 14, 15, 16, tz="local") + expected = u"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 "{}".format(d) == "1975-12-25T14:15:16+01:00" + assert "{:YYYY}".format(d) == "1975" + assert "{:%Y}".format(d) == "1975" + assert "{:%H:%M %d.%m.%Y}".format(d) == "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..61d9a8b --- /dev/null +++ b/tests/datetime/test_sub.py @@ -0,0 +1,248 @@ +from datetime import timedelta + +import pendulum +import pytest + +from ..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 2012 == d.year + assert 12 == d.month + assert 8 == d.day + assert 3 == d.hour + assert 11 == d.minute + assert 59 == d.second + assert 123456 == d.microsecond + + +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", dst_rule=pendulum.PRE_TRANSITION + ) + 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..96398e3 --- /dev/null +++ b/tests/datetime/test_timezone.py @@ -0,0 +1,36 @@ +import pendulum + +from ..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..40a96af --- /dev/null +++ b/tests/duration/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/duration/test_add_sub.py b/tests/duration/test_add_sub.py new file mode 100644 index 0000000..530343a --- /dev/null +++ b/tests/duration/test_add_sub.py @@ -0,0 +1,52 @@ +from datetime import timedelta + +import pendulum + +from ..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..fad38b6 --- /dev/null +++ b/tests/duration/test_arithmetic.py @@ -0,0 +1,71 @@ +import pendulum + +from ..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(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) + + +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..115e397 --- /dev/null +++ b/tests/duration/test_behavior.py @@ -0,0 +1,19 @@ +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..31ca0ea --- /dev/null +++ b/tests/duration/test_construct.py @@ -0,0 +1,97 @@ +from datetime import timedelta + +import pendulum +import pytest + +from pendulum.duration import AbsoluteDuration + +from ..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 730 == pi.days + assert 63072000 == pi.total_seconds() + + +def test_months(): + pi = pendulum.duration(months=3) + assert_duration(pi, months=3, weeks=0) + assert 90 == pi.days + assert 7776000 == pi.total_seconds() + + +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 1997 == pi.days + assert 7285 == pi.seconds + + +def test_absolute_interval(): + pi = AbsoluteDuration(days=-1177, seconds=-7284, microseconds=-1000001) + assert_duration(pi, 0, 0, 168, 1, 2, 1, 25) + assert 1 == pi.microseconds + 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 3456.123456 == delta.total_seconds() + assert 3456 == delta.seconds + + +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..85ad3e7 --- /dev/null +++ b/tests/duration/test_in_methods.py @@ -0,0 +1,26 @@ +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..499e3c3 --- /dev/null +++ b/tests/duration/test_in_words.py @@ -0,0 +1,75 @@ +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..9c2897e --- /dev/null +++ b/tests/duration/test_total_methods.py @@ -0,0 +1,26 @@ +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..40a96af --- /dev/null +++ b/tests/fixtures/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/fixtures/tz/Paris b/tests/fixtures/tz/Paris Binary files differnew file mode 100644 index 0000000..cf6e2e2 --- /dev/null +++ b/tests/fixtures/tz/Paris 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 Binary files differnew file mode 100644 index 0000000..cf6e2e2 --- /dev/null +++ b/tests/fixtures/tz/symlink/usr/share/zoneinfo/Europe/Paris diff --git a/tests/formatting/__init__.py b/tests/formatting/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/tests/formatting/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/formatting/test_formatter.py b/tests/formatting/test_formatter.py new file mode 100644 index 0000000..dd44f4d --- /dev/null +++ b/tests/formatting/test_formatter.py @@ -0,0 +1,261 @@ +# -*- coding: utf-8 -*- +import pendulum +import pytest + +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") == u"28 août 2016" + assert f.format(d, "LLL", locale="fr") == u"28 août 2016 07:03" + assert f.format(d, "LLLL", locale="fr") == u"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..40a96af --- /dev/null +++ b/tests/helpers/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/helpers/test_local_time.py b/tests/helpers/test_local_time.py new file mode 100644 index 0000000..a9afbd0 --- /dev/null +++ b/tests/helpers/test_local_time.py @@ -0,0 +1,29 @@ +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/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_da.py b/tests/localization/test_da.py new file mode 100644 index 0000000..aff4034 --- /dev/null +++ b/tests/localization/test_da.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pendulum + + +locale = "da" + + +def test_diff_for_humans(): + with pendulum.test(pendulum.datetime(2016, 8, 29)): + 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..babcc65 --- /dev/null +++ b/tests/localization/test_de.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pendulum + + +locale = "de" + + +def test_diff_for_humans(): + with pendulum.test(pendulum.datetime(2016, 8, 29)): + 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..c75d0bc --- /dev/null +++ b/tests/localization/test_es.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pendulum + + +locale = "es" + + +def test_diff_for_humans(): + with pendulum.test(pendulum.datetime(2016, 8, 29)): + 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..c14f320 --- /dev/null +++ b/tests/localization/test_fa.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pendulum + + +locale = "fa" + + +def test_diff_for_humans(): + with pendulum.test(pendulum.datetime(2016, 8, 29)): + 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..376d090 --- /dev/null +++ b/tests/localization/test_fo.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pendulum + + +locale = "fo" + + +def test_diff_for_humans(): + with pendulum.test(pendulum.datetime(2016, 8, 29)): + 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..68d1158 --- /dev/null +++ b/tests/localization/test_fr.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pendulum + + +locale = "fr" + + +def test_diff_for_humans(): + with pendulum.test(pendulum.datetime(2016, 8, 29)): + 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_id.py b/tests/localization/test_id.py new file mode 100644 index 0000000..5b18a67 --- /dev/null +++ b/tests/localization/test_id.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pendulum + + +locale = "id" + + +def test_diff_for_humans(): + with pendulum.test(pendulum.datetime(2016, 8, 29)): + 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..6db66c9 --- /dev/null +++ b/tests/localization/test_it.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pendulum + + +locale = "it" + + +def test_diff_for_humans(): + with pendulum.test(pendulum.datetime(2016, 8, 29)): + 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_ko.py b/tests/localization/test_ko.py new file mode 100644 index 0000000..63f03f4 --- /dev/null +++ b/tests/localization/test_ko.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pendulum + + +locale = "ko" + + +def test_diff_for_humans(): + with pendulum.test(pendulum.datetime(2016, 8, 29)): + 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..231c55d --- /dev/null +++ b/tests/localization/test_lt.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pendulum + + +locale = "lt" + + +def test_diff_for_humans(): + with pendulum.test(pendulum.datetime(2016, 8, 29)): + 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..3cd7e8e --- /dev/null +++ b/tests/localization/test_nb.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pendulum + + +locale = "nb" + + +def test_diff_for_humans(): + with pendulum.test(pendulum.datetime(2016, 8, 29)): + 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..545c8a3 --- /dev/null +++ b/tests/localization/test_nl.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pendulum + + +locale = "nl" + + +def test_diff_for_humans(): + with pendulum.test(pendulum.datetime(2016, 8, 29)): + 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..5105c75 --- /dev/null +++ b/tests/localization/test_nn.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pendulum + + +locale = "nn" + + +def test_diff_for_humans(): + with pendulum.test(pendulum.datetime(2016, 8, 29)): + 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..926ee8a --- /dev/null +++ b/tests/localization/test_pl.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pendulum + + +locale = "pl" + + +def test_diff_for_humans(): + with pendulum.test(pendulum.datetime(2016, 8, 29)): + 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..c257f11 --- /dev/null +++ b/tests/localization/test_ru.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pendulum + + +locale = "ru" + + +def test_diff_for_humans(): + with pendulum.test(pendulum.datetime(2016, 8, 29)): + 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/parsing/__init__.py b/tests/parsing/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/tests/parsing/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/parsing/test_parse_iso8601.py b/tests/parsing/test_parse_iso8601.py new file mode 100644 index 0000000..d86cb62 --- /dev/null +++ b/tests/parsing/test_parse_iso8601.py @@ -0,0 +1,464 @@ +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..bcd7a53 --- /dev/null +++ b/tests/parsing/test_parsing.py @@ -0,0 +1,684 @@ +import datetime + +import pendulum +import pytest + +from pendulum.parsing import ParserError +from pendulum.parsing import parse + + +def test_y(): + text = "2016" + + parsed = parse(text) + + assert 2016 == parsed.year + assert 1 == parsed.month + assert 1 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + +def test_ym(): + text = "2016-10" + + parsed = parse(text) + + assert 2016 == parsed.year + assert 10 == parsed.month + assert 1 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + +def test_ymd(): + text = "2016-10-06" + + parsed = parse(text) + + assert 2016 == parsed.year + assert 10 == parsed.month + assert 6 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + +def test_ymd_one_character(): + text = "2016-2-6" + + parsed = parse(text, strict=False) + + assert 2016 == parsed.year + assert 2 == parsed.month + assert 6 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + +def test_ymd_hms(): + text = "2016-10-06 12:34:56" + + parsed = parse(text) + + assert 2016 == parsed.year + assert 10 == parsed.month + assert 6 == parsed.day + assert 12 == parsed.hour + assert 34 == parsed.minute + assert 56 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "2016-10-06 12:34:56.123456" + + parsed = parse(text) + + assert 2016 == parsed.year + assert 10 == parsed.month + assert 6 == parsed.day + assert 12 == parsed.hour + assert 34 == parsed.minute + assert 56 == parsed.second + assert 123456 == parsed.microsecond + assert parsed.tzinfo is None + + +def test_rfc_3339(): + text = "2016-10-06T12:34:56+05:30" + + parsed = parse(text) + + assert 2016 == parsed.year + assert 10 == parsed.month + assert 6 == parsed.day + assert 12 == parsed.hour + assert 34 == parsed.minute + assert 56 == parsed.second + assert 0 == parsed.microsecond + assert 19800 == parsed.utcoffset().total_seconds() + + +def test_rfc_3339_extended(): + text = "2016-10-06T12:34:56.123456+05:30" + + parsed = parse(text) + + assert 2016 == parsed.year + assert 10 == parsed.month + assert 6 == parsed.day + assert 12 == parsed.hour + assert 34 == parsed.minute + assert 56 == parsed.second + assert 123456 == parsed.microsecond + assert 19800 == parsed.utcoffset().total_seconds() + + text = "2016-10-06T12:34:56.000123+05:30" + + parsed = parse(text) + + assert 2016 == parsed.year + assert 10 == parsed.month + assert 6 == parsed.day + assert 12 == parsed.hour + assert 34 == parsed.minute + assert 56 == parsed.second + assert 123 == parsed.microsecond + assert 19800 == parsed.utcoffset().total_seconds() + + +def test_rfc_3339_extended_nanoseconds(): + text = "2016-10-06T12:34:56.123456789+05:30" + + parsed = parse(text) + + assert 2016 == parsed.year + assert 10 == parsed.month + assert 6 == parsed.day + assert 12 == parsed.hour + assert 34 == parsed.minute + assert 56 == parsed.second + assert 123456 == parsed.microsecond + assert 19800 == parsed.utcoffset().total_seconds() + + +def test_iso_8601_date(): + text = "2012" + + parsed = parse(text) + + assert 2012 == parsed.year + assert 1 == parsed.month + assert 1 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "2012-05-03" + + parsed = parse(text) + + assert 2012 == parsed.year + assert 5 == parsed.month + assert 3 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "20120503" + + parsed = parse(text) + + assert 2012 == parsed.year + assert 5 == parsed.month + assert 3 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "2012-05" + + parsed = parse(text) + + assert 2012 == parsed.year + assert 5 == parsed.month + assert 1 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + +def test_iso8601_datetime(): + text = "2016-10-01T14" + + parsed = parse(text) + + assert 2016 == parsed.year + assert 10 == parsed.month + assert 1 == parsed.day + assert 14 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "2016-10-01T14:30" + + parsed = parse(text) + + assert 2016 == parsed.year + assert 10 == parsed.month + assert 1 == parsed.day + assert 14 == parsed.hour + assert 30 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "20161001T14" + + parsed = parse(text) + + assert 2016 == parsed.year + assert 10 == parsed.month + assert 1 == parsed.day + assert 14 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "20161001T1430" + + parsed = parse(text) + + assert 2016 == parsed.year + assert 10 == parsed.month + assert 1 == parsed.day + assert 14 == parsed.hour + assert 30 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "20161001T1430+0530" + + parsed = parse(text) + + assert 2016 == parsed.year + assert 10 == parsed.month + assert 1 == parsed.day + assert 14 == parsed.hour + assert 30 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert 19800 == parsed.utcoffset().total_seconds() + + text = "20161001T1430,4+0530" + + parsed = parse(text) + + assert 2016 == parsed.year + assert 10 == parsed.month + assert 1 == parsed.day + assert 14 == parsed.hour + assert 30 == parsed.minute + assert 0 == parsed.second + assert 400000 == parsed.microsecond + assert 19800 == parsed.utcoffset().total_seconds() + + text = "2008-09-03T20:56:35.450686+01" + + parsed = parse(text) + + assert 2008 == parsed.year + assert 9 == parsed.month + assert 3 == parsed.day + assert 20 == parsed.hour + assert 56 == parsed.minute + assert 35 == parsed.second + assert 450686 == parsed.microsecond + assert 3600 == parsed.utcoffset().total_seconds() + + +def test_iso8601_week_number(): + text = "2012-W05" + + parsed = parse(text) + + assert 2012 == parsed.year + assert 1 == parsed.month + assert 30 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "2012W05" + + parsed = parse(text) + + assert 2012 == parsed.year + assert 1 == parsed.month + assert 30 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + # Long Year + text = "2015W53" + + parsed = parse(text) + + assert 2015 == parsed.year + assert 12 == parsed.month + assert 28 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "2012-W05-5" + + parsed = parse(text) + + assert 2012 == parsed.year + assert 2 == parsed.month + assert 3 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "2012W055" + + parsed = parse(text) + + assert 2012 == parsed.year + assert 2 == parsed.month + assert 3 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "2009-W53-7" + parsed = parse(text) + + assert 2010 == parsed.year + assert 1 == parsed.month + assert 3 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "2009-W01-1" + parsed = parse(text) + + assert 2008 == parsed.year + assert 12 == parsed.month + assert 29 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + +def test_iso8601_week_number_with_time(): + text = "2012-W05T09" + + parsed = parse(text) + + assert 2012 == parsed.year + assert 1 == parsed.month + assert 30 == parsed.day + assert 9 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "2012W05T09" + + parsed = parse(text) + + assert 2012 == parsed.year + assert 1 == parsed.month + assert 30 == parsed.day + assert 9 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "2012-W05-5T09" + + parsed = parse(text) + + assert 2012 == parsed.year + assert 2 == parsed.month + assert 3 == parsed.day + assert 9 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "2012W055T09" + + parsed = parse(text) + + assert 2012 == parsed.year + assert 2 == parsed.month + assert 3 == parsed.day + assert 9 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + +def test_iso8601_ordinal(): + text = "2012-007" + + parsed = parse(text) + + assert 2012 == parsed.year + assert 1 == parsed.month + assert 7 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "2012007" + + parsed = parse(text) + + assert 2012 == parsed.year + assert 1 == parsed.month + assert 7 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + +def test_iso8601_time(): + now = pendulum.datetime(2015, 11, 12) + + text = "201205" + + parsed = parse(text, now=now) + + assert 2015 == parsed.year + assert 11 == parsed.month + assert 12 == parsed.day + assert 20 == parsed.hour + assert 12 == parsed.minute + assert 5 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "20:12:05" + + parsed = parse(text, now=now) + + assert 2015 == parsed.year + assert 11 == parsed.month + assert 12 == parsed.day + assert 20 == parsed.hour + assert 12 == parsed.minute + assert 5 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "20:12:05.123456" + + parsed = parse(text, now=now) + + assert 2015 == parsed.year + assert 11 == parsed.month + assert 12 == parsed.day + assert 20 == parsed.hour + assert 12 == parsed.minute + assert 5 == parsed.second + assert 123456 == parsed.microsecond + 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 2012 == parsed.year + assert 1 == parsed.month + assert 1 == parsed.day + + text = "2012-03" + + parsed = parse(text, exact=True) + + assert isinstance(parsed, datetime.date) + assert 2012 == parsed.year + assert 3 == parsed.month + assert 1 == parsed.day + + text = "2012-03-13" + + parsed = parse(text, exact=True) + + assert isinstance(parsed, datetime.date) + assert 2012 == parsed.year + assert 3 == parsed.month + assert 13 == parsed.day + + text = "2012W055" + + parsed = parse(text, exact=True) + + assert isinstance(parsed, datetime.date) + assert 2012 == parsed.year + assert 2 == parsed.month + assert 3 == parsed.day + + text = "2012007" + + parsed = parse(text, exact=True) + + assert isinstance(parsed, datetime.date) + assert 2012 == parsed.year + assert 1 == parsed.month + assert 7 == parsed.day + + text = "20:12:05" + + parsed = parse(text, exact=True) + + assert isinstance(parsed, datetime.time) + assert 20 == parsed.hour + assert 12 == parsed.minute + assert 5 == parsed.second + assert 0 == parsed.microsecond + + +def test_edge_cases(): + text = "2013-11-1" + + parsed = parse(text, strict=False) + assert 2013 == parsed.year + assert 11 == parsed.month + assert 1 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "10-01-01" + + parsed = parse(text, strict=False) + assert 2010 == parsed.year + assert 1 == parsed.month + assert 1 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "31-01-01" + + parsed = parse(text, strict=False) + assert 2031 == parsed.year + assert 1 == parsed.month + assert 1 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + assert parsed.tzinfo is None + + text = "32-01-01" + + parsed = parse(text, strict=False) + assert 2032 == parsed.year + assert 1 == parsed.month + assert 1 == parsed.day + assert 0 == parsed.hour + assert 0 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + 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 2015 == parsed.year + assert 8 == parsed.month + assert 4 == parsed.day + assert 23 == parsed.hour + assert 20 == parsed.minute + assert 0 == parsed.second + assert 0 == parsed.microsecond + 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 2016 == parsed.year + assert 12 == parsed.month + assert 26 == parsed.day + assert 15 == parsed.hour + assert 45 == parsed.minute + assert 28 == parsed.second diff --git a/tests/parsing/test_parsing_duration.py b/tests/parsing/test_parsing_duration.py new file mode 100644 index 0000000..41d488b --- /dev/null +++ b/tests/parsing/test_parsing_duration.py @@ -0,0 +1,296 @@ +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/period/__init__.py b/tests/period/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/tests/period/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/period/test_add_subtract.py b/tests/period/test_add_subtract.py new file mode 100644 index 0000000..d9a739e --- /dev/null +++ b/tests/period/test_add_subtract.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +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) + + print((start - end).in_words()) + + period = end - start + print(period.in_words()) + new_end = start + period + + assert new_end == end diff --git a/tests/period/test_arithmetic.py b/tests/period/test_arithmetic.py new file mode 100644 index 0000000..47b6ddf --- /dev/null +++ b/tests/period/test_arithmetic.py @@ -0,0 +1,51 @@ +import pendulum + +from ..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.period(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.period(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.period(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.period(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.period(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.period(dt1, dt2) + mul = it // 3 + assert isinstance(mul, pendulum.Duration) + assert_duration(mul, 0, 0, 0, 0, 16, 0, 11) diff --git a/tests/period/test_behavior.py b/tests/period/test_behavior.py new file mode 100644 index 0000000..7ad7b45 --- /dev/null +++ b/tests/period/test_behavior.py @@ -0,0 +1,52 @@ +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.period(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.period(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.period(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/period/test_construct.py b/tests/period/test_construct.py new file mode 100644 index 0000000..21538fc --- /dev/null +++ b/tests/period/test_construct.py @@ -0,0 +1,119 @@ +from datetime import datetime + +import pendulum + +from ..conftest import assert_datetime + + +def test_with_datetimes(): + dt1 = datetime(2000, 1, 1) + dt2 = datetime(2000, 1, 31) + p = pendulum.period(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.period(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.period(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.period(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.period(dt1, dt3) + p2 = pendulum.period(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.period(dt1, dt3) + p2 = pendulum.period(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/period/test_hashing.py b/tests/period/test_hashing.py new file mode 100644 index 0000000..2a567f1 --- /dev/null +++ b/tests/period/test_hashing.py @@ -0,0 +1,21 @@ +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/period/test_in_words.py b/tests/period/test_in_words.py new file mode 100644 index 0000000..7bedbc7 --- /dev/null +++ b/tests/period/test_in_words.py @@ -0,0 +1,66 @@ +import pendulum + + +def test_week(): + start_date = pendulum.datetime(2012, 1, 1) + period = pendulum.period(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.period(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.period( + 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.period( + 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.period(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.period( + 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.period(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.period( + 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.period(start=start_date, end=start_date) + assert period.in_words() == "0 microseconds" diff --git a/tests/period/test_range.py b/tests/period/test_range.py new file mode 100644 index 0000000..55aac10 --- /dev/null +++ b/tests/period/test_range.py @@ -0,0 +1,118 @@ +import pendulum + +from pendulum import Period + +from ..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 = Period(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 = Period(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 = Period(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 = Period(dt1, dt2) + i = 0 + 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.period(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.period(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.period(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.period(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.period(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.period(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/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..09a98a8 --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,212 @@ +from __future__ import absolute_import + +from datetime import datetime + +import pendulum +import pytest +import pytz + +from pendulum import timezone +from pendulum.helpers import days_in_year +from pendulum.helpers import precise_diff +from pendulum.helpers import week_day + +from .conftest import assert_datetime + + +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 5 == week_day(2017, 6, 2) + assert 7 == week_day(2017, 1, 1) + + +def test_days_in_years(): + assert 365 == days_in_year(2017) + assert 366 == days_in_year(2016) + + +def test_test_now(): + now = pendulum.datetime(2000, 11, 10, 12, 34, 56, 123456) + pendulum.set_test_now(now) + + assert pendulum.has_test_now() + assert now == pendulum.get_test_now() + + assert_datetime(pendulum.now(), 2000, 11, 10, 12, 34, 56, 123456) + + pendulum.set_test_now() + + assert not pendulum.has_test_now() + assert pendulum.get_test_now() is None + + +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) + + +def test_with_test(): + t = pendulum.datetime(2000, 1, 1) + + with pendulum.test(t): + assert pendulum.now() == t + + assert pendulum.now() != t + + # Also make sure that it restores things after an exception + with pytest.raises(RuntimeError): + with pendulum.test(t): + assert pendulum.now() == t + raise RuntimeError + + assert pendulum.now() != t diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..becc496 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,11 @@ +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 "Europe/Paris" == tz.name diff --git a/tests/test_parsing.py b/tests/test_parsing.py new file mode 100644 index 0000000..3dcf050 --- /dev/null +++ b/tests/test_parsing.py @@ -0,0 +1,139 @@ +import pendulum + +from .conftest import assert_date +from .conftest import assert_datetime +from .conftest import assert_duration +from .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 "+01:30" == dt.tz.name + assert 5400 == dt.offset + + text = "2016-10-16" + + dt = pendulum.parse(text) + + assert isinstance(dt, pendulum.DateTime) + assert_datetime(dt, 2016, 10, 16, 0, 0, 0, 0) + assert 0 == dt.offset + + with pendulum.test(pendulum.datetime(2015, 11, 12)): + 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 0 == dt.offset + + +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 "Europe/Paris" == dt.tz.name + assert 7200 == dt.offset + + +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 5400 == dt.offset + + 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.Period) + 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.Period) + 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.Period) + 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.test(mock_now): + assert pendulum.parse("now") == mock_now + + +def test_parse_with_utc_timezone(): + dt = pendulum.parse("2020-02-05T20:05:37.364951Z") + + assert "2020-02-05T20:05:37.364951Z" == dt.to_iso8601_string() diff --git a/tests/time/__init__.py b/tests/time/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/tests/time/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/time/test_add.py b/tests/time/test_add.py new file mode 100644 index 0000000..05998a3 --- /dev/null +++ b/tests/time/test_add.py @@ -0,0 +1,75 @@ +from datetime import timedelta + +import pendulum +import pytest + + +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..4296434 --- /dev/null +++ b/tests/time/test_behavior.py @@ -0,0 +1,46 @@ +import pickle + +from datetime import time + +import pendulum +import pytest + +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..b163ecb --- /dev/null +++ b/tests/time/test_comparison.py @@ -0,0 +1,183 @@ +from datetime import time + +import pendulum + +from ..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..d2d106f --- /dev/null +++ b/tests/time/test_construct.py @@ -0,0 +1,20 @@ +import pendulum + +from ..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..5e99701 --- /dev/null +++ b/tests/time/test_diff.py @@ -0,0 +1,348 @@ +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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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.test(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..1337ec5 --- /dev/null +++ b/tests/time/test_fluent_setters.py @@ -0,0 +1,11 @@ +from pendulum import Time + +from ..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..23be7db --- /dev/null +++ b/tests/time/test_strings.py @@ -0,0 +1,37 @@ +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 "{}".format(d) == "14:15:16" + assert "{:mm}".format(d) == "15" diff --git a/tests/time/test_sub.py b/tests/time/test_sub.py new file mode 100644 index 0000000..05e4b10 --- /dev/null +++ b/tests/time/test_sub.py @@ -0,0 +1,110 @@ +from datetime import time +from datetime import timedelta + +import pendulum +import pytest +import pytz + +from pendulum import Time + +from ..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..40a96af --- /dev/null +++ b/tests/tz/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/tz/test_helpers.py b/tests/tz/test_helpers.py new file mode 100644 index 0000000..c8b8c92 --- /dev/null +++ b/tests/tz/test_helpers.py @@ -0,0 +1,25 @@ +import pytest + +from pendulum.tz import timezone +from pendulum.tz.timezone import FixedTimezone +from pendulum.tz.timezone import Timezone +from pendulum.tz.zoneinfo.exceptions import InvalidTimezone + + +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..fdf1cc2 --- /dev/null +++ b/tests/tz/test_local_timezone.py @@ -0,0 +1,38 @@ +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 diff --git a/tests/tz/test_timezone.py b/tests/tz/test_timezone.py new file mode 100644 index 0000000..303a5fc --- /dev/null +++ b/tests/tz/test_timezone.py @@ -0,0 +1,591 @@ +from datetime import datetime +from datetime import timedelta + +import pendulum +import pytest + +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 PY36 + +from ..conftest import assert_datetime + + +@pytest.fixture(autouse=True) +def setup(): + pendulum.tz._tz_cache = {} + + yield + + pendulum.tz._tz_cache = {} + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +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) + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +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() + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +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_explicit_post_rule(): + dt = datetime(2013, 3, 31, 2, 30, 45, 123456) + tz = timezone("Europe/Paris") + dt = tz.convert(dt, dst_rule=pendulum.POST_TRANSITION) + + 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_explicit_pre_rule(): + dt = datetime(2013, 3, 31, 2, 30, 45, 123456) + tz = timezone("Europe/Paris") + dt = tz.convert(dt, dst_rule=pendulum.PRE_TRANSITION) + + 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_error(): + dt = datetime(2013, 3, 31, 2, 30, 45, 123456) + tz = timezone("Europe/Paris") + with pytest.raises(NonExistingTime): + tz.convert(dt, dst_rule=pendulum.TRANSITION_ERROR) + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +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() + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +def test_repeated_time_explicit_post_rule(): + dt = datetime(2013, 10, 27, 2, 30, 45, 123456) + tz = timezone("Europe/Paris") + dt = tz.convert(dt, dst_rule=pendulum.POST_TRANSITION) + + 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() + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +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) + + +@pytest.mark.skipif(not PY36, reason="Disambiguation is not available in Python 2.7") +def test_repeated_time_explicit_pre_rule(): + dt = datetime(2013, 10, 27, 2, 30, 45, 123456) + tz = timezone("Europe/Paris") + dt = tz.convert(dt, dst_rule=pendulum.PRE_TRANSITION) + + 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, dst_rule=pendulum.TRANSITION_ERROR) + + +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_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", + dst_rule=pendulum.PRE_TRANSITION, + ) + + 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", + dst_rule=pendulum.TRANSITION_ERROR, + ) + + +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", + dst_rule=pendulum.PRE_TRANSITION, + ) + + 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", + dst_rule=pendulum.TRANSITION_ERROR, + ) + + +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=64800) + + +def test_dst(): + tz = pendulum.timezone("Europe/Amsterdam") + dst = tz.dst(datetime(1940, 7, 1)) + + assert dst == timedelta(0, 6000) + + +def test_short_timezones(): + tz = pendulum.timezone("CET") + assert len(tz._transitions) > 0 + + tz = pendulum.timezone("EET") + assert len(tz._transitions) > 0 + + +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 + + +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 + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +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" + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +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 + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +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 + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +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" + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +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" + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +def test_convert_sets_fold_attribute_properly(): + tz = pendulum.timezone("US/Eastern") + + dt = tz.convert(datetime(2014, 11, 2, 1, 30), dst_rule=pendulum.PRE_TRANSITION) + assert dt.fold == 0 + + dt = tz.convert(datetime(2014, 11, 2, 1, 30), dst_rule=pendulum.POST_TRANSITION) + assert dt.fold == 1 + + +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 18000 == tz2.utcoffset(dt).total_seconds() + assert tz2.dst(dt) == timedelta() + + +def test_just_before_last_transition(): + tz = pendulum.timezone("Asia/Shanghai") + dt = datetime(1991, 4, 20, 1, 49, 8) + dt = tz.convert(dt, dst_rule=pendulum.POST_TRANSITION) + + epoch = datetime(1970, 1, 1, tzinfo=timezone("UTC")) + expected = (dt - epoch).total_seconds() + assert expected == 672079748.0 + + +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), dst_rule=pendulum.POST_TRANSITION + ) + + 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), dst_rule=pendulum.PRE_TRANSITION + ) + + 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), dst_rule=pendulum.POST_TRANSITION + ) + + assert_datetime(dt, 2134, 10, 31, 2, 30) + assert dt.utcoffset().total_seconds() == 3600 + assert dt.dst() == timedelta() + + +def test_timezones_extension_can_be_disabled(): + tz = pendulum.timezone("Europe/Paris", extended=False) + 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), dst_rule=pendulum.POST_TRANSITION + ) + + assert_datetime(dt, 2134, 3, 28, 2, 30) + assert dt.utcoffset().total_seconds() == 3600 + assert dt.dst() == timedelta() + + dt = tz.convert(pendulum.naive(2134, 7, 11, 2, 30)) + + assert_datetime(dt, 2134, 7, 11, 2, 30) + assert dt.utcoffset().total_seconds() == 3600 + assert dt.dst() == timedelta() + + dt = tz.convert( + pendulum.naive(2134, 10, 31, 2, 30), dst_rule=pendulum.PRE_TRANSITION + ) + + assert_datetime(dt, 2134, 10, 31, 2, 30) + assert dt.utcoffset().total_seconds() == 3600 + assert dt.dst() == timedelta() + + dt = tz.convert( + pendulum.naive(2134, 10, 31, 2, 30), dst_rule=pendulum.POST_TRANSITION + ) + + 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 "Timezone('Europe/Paris')" == repr(tz) diff --git a/tests/tz/test_timezones.py b/tests/tz/test_timezones.py new file mode 100644 index 0000000..8c90674 --- /dev/null +++ b/tests/tz/test_timezones.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +import pendulum +import pytest + + +def test_timezones(): + zones = pendulum.timezones + + assert "America/Argentina/Buenos_Aires" in zones + + +@pytest.mark.parametrize("zone", [zone for zone in pendulum.timezones]) +def test_timezones_are_loadable(zone): + pendulum.timezone(zone) diff --git a/tests/tz/zoneinfo/__init__.py b/tests/tz/zoneinfo/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/tz/zoneinfo/__init__.py diff --git a/tests/tz/zoneinfo/test_posix_timezone.py b/tests/tz/zoneinfo/test_posix_timezone.py new file mode 100644 index 0000000..41a6458 --- /dev/null +++ b/tests/tz/zoneinfo/test_posix_timezone.py @@ -0,0 +1,65 @@ +from pendulum.tz.zoneinfo.posix_timezone import JPosixTransition +from pendulum.tz.zoneinfo.posix_timezone import MPosixTransition +from pendulum.tz.zoneinfo.posix_timezone import posix_spec + + +def test_posix_spec_m(): + spec = "CET-1CEST,M3.5.0,M10.5.0/3" + tz = posix_spec(spec) + + assert tz.std_abbr == "CET" + assert tz.std_offset == 3600 + assert tz.dst_abbr == "CEST" + assert tz.dst_offset == 7200 + + assert isinstance(tz.dst_start, MPosixTransition) + assert tz.dst_start.month == 3 + assert tz.dst_start.week == 5 + assert tz.dst_start.weekday == 0 + assert tz.dst_start.offset == 7200 + + assert isinstance(tz.dst_end, MPosixTransition) + assert tz.dst_end.month == 10 + assert tz.dst_end.week == 5 + assert tz.dst_end.weekday == 0 + assert tz.dst_end.offset == 3 * 3600 + + +def test_posix_spec_m_no_abbr(): + spec = "<+12>-12<+13>,M11.1.0,M1.2.1/147" + tz = posix_spec(spec) + + assert tz.std_abbr == "+12" + assert tz.std_offset == 12 * 3600 + assert tz.dst_abbr == "+13" + assert tz.dst_offset == 13 * 3600 + + assert isinstance(tz.dst_start, MPosixTransition) + assert tz.dst_start.month == 11 + assert tz.dst_start.week == 1 + assert tz.dst_start.weekday == 0 + assert tz.dst_start.offset == 7200 + + assert isinstance(tz.dst_end, MPosixTransition) + assert tz.dst_end.month == 1 + assert tz.dst_end.week == 2 + assert tz.dst_end.weekday == 1 + assert tz.dst_end.offset == 147 * 3600 + + +def test_posix_spec_j_no_abbr(): + spec = "<+0330>-3:30<+0430>,J80/0,J264/0" + tz = posix_spec(spec) + + assert tz.std_abbr == "+0330" + assert tz.std_offset == 3 * 3600 + 30 * 60 + assert tz.dst_abbr == "+0430" + assert tz.dst_offset == 4 * 3600 + 30 * 60 + + assert isinstance(tz.dst_start, JPosixTransition) + assert tz.dst_start.day == 80 + assert tz.dst_start.offset == 0 + + assert isinstance(tz.dst_end, JPosixTransition) + assert tz.dst_end.day == 264 + assert tz.dst_end.offset == 0 diff --git a/tests/tz/zoneinfo/test_reader.py b/tests/tz/zoneinfo/test_reader.py new file mode 100644 index 0000000..a19ba9a --- /dev/null +++ b/tests/tz/zoneinfo/test_reader.py @@ -0,0 +1,46 @@ +import os + +import pytest + +from pendulum.tz.zoneinfo.exceptions import InvalidTimezone +from pendulum.tz.zoneinfo.exceptions import InvalidZoneinfoFile +from pendulum.tz.zoneinfo.reader import Reader +from pendulum.tz.zoneinfo.timezone import Timezone + + +def test_read_for_bad_timezone(): + reader = Reader() + with pytest.raises(InvalidTimezone): + reader.read_for("---NOT A TIMEZONE---") + + +def test_read_for_valid(): + reader = Reader() + + tz = reader.read_for("America/Toronto") + assert isinstance(tz, Timezone) + + +def test_read(): + reader = Reader() + local_path = os.path.join(os.path.split(__file__)[0], "..", "..") + tz_file = os.path.join(local_path, "fixtures", "tz", "Paris") + tz = reader.read(tz_file) + + assert len(tz.transitions) > 0 + + +def test_read_invalid(): + reader = Reader() + local_path = os.path.join(os.path.split(__file__)[0], "..") + tz_file = os.path.join(local_path, "fixtures", "tz", "NOT_A_TIMEZONE") + + with pytest.raises(InvalidZoneinfoFile): + reader.read(tz_file) + + +def test_set_transitions_for_no_transition_database_file(): + reader = Reader() + tz = reader.read_for("Etc/UTC") + + assert len(tz.transitions) == 1 @@ -0,0 +1,18 @@ +[tox] +isolated_build = true +envlist = py27, py35, py36, py37, py38, pypy, 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/ |