Repository: sdispater/pendulum
Pull request #721 (2023-07-18 19:42:01)

Rewrite extensions in Rust
This PR is the first introduction to Rust in the codebase.

It is a complete rewrite of existing C extensions and paves the way to more speed improvements by rewriting other parts of the codebase in the future.

Note that if there is still a pure Python version of these extensions and will always be for the foreseeable future.

Be aware that this PR introduces small backwards  incompatibilities when parsing ISO8601 strings:

- Basic time representation now requires a leading `T`, for instance `T201205`
- Fractional hours and minutes are no longer supported. It was technically supported before the behavior was incorrect so support has been removed for now but will be reintroduced in the future.

## Pull Request Check List

- [x] Added **tests** for changed code.
- [ ] Updated **documentation** for changed code.


codspeed-hq[bot]: ## [CodSpeed Performance Report](https://codspeed.io/sdispater/pendulum/branches/refactor/rust)

### Merging #721 will **not alter performance**

<sub>Comparing <code>refactor/rust</code> (ef971e4) with <code>master</code> (c4e518c)</sub>



### Summary

`✅ 2` untouched benchmarks





sdispater: Thanks for the thorough review @Maneren! I applied all of your suggestions.
Maneren: Happy to help

Patch:
diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000..f0ba8af
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,15 @@
+[build]
+rustflags = []
+
+# see https://pyo3.rs/main/building_and_distribution.html#macos
+[target.x86_64-apple-darwin]
+rustflags = [
+  "-C", "link-arg=-undefined",
+  "-C", "link-arg=dynamic_lookup",
+]
+
+[target.aarch64-apple-darwin]
+rustflags = [
+  "-C", "link-arg=-undefined",
+  "-C", "link-arg=dynamic_lookup",
+]
diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml
index 3d601ce..df1754b 100644
--- a/.github/workflows/codspeed.yml
+++ b/.github/workflows/codspeed.yml
@@ -25,21 +25,22 @@ jobs:
 
       - name: Install poetry
         run: |
-          curl -fsS https://install.python-poetry.org | python - --preview -y
+          curl -fsS https://install.python-poetry.org | python - -y
 
       - name: Update PATH
         if: ${{ matrix.os != 'Windows' }}
         run: echo "$HOME/.local/bin" >> $GITHUB_PATH
 
-      - name: Update Path for Windows
-        if: ${{ matrix.os == 'Windows' }}
-        run: echo "$APPDATA\Python\Scripts" >> $GITHUB_PATH
-
       - name: Configure poetry
         run: poetry config virtualenvs.create false
 
       - name: Install dependencies
-        run: poetry install --only main --only test --only benchmark -vvv
+        run: poetry install --only test --only benchmark -vvv --no-root
+
+      - name: Install pendulum and check extensions
+        run: |
+          MATURIN_BUILD_ARGS="--no-default-features -vv" pip install -e . -v
+          python -c 'import _pendulum; assert _pendulum.__pendulum_default_allocator__'
 
       - name: Run benchmarks
         uses: CodSpeedHQ/action@v1
diff --git a/.gitignore b/.gitignore
index bb25f8b..d7309a0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,3 +35,4 @@ setup.py
 # editor
 
 .vscode
+/target
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2974fac..c5bd461 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -12,11 +12,20 @@ repos:
       - id: debug-statements
 
   - repo: https://github.com/psf/black
-    rev: 23.3.0
+    rev: 23.7.0
     hooks:
       - id: black
 
-  - repo: https://github.com/charliermarsh/ruff-pre-commit
-    rev: v0.0.270
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.0.283
     hooks:
     -   id: ruff
+
+  - repo: local
+    hooks:
+      - id: lint-rust
+        name: Lint Rust
+        entry: make lint-rust
+        types: [rust]
+        language: rust
+        pass_filenames: false
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..4d0fd72
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,318 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "indoc"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306"
+
+[[package]]
+name = "libc"
+version = "0.2.139"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+
+[[package]]
+name = "libmimalloc-sys"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4ac0e912c8ef1b735e92369695618dc5b1819f5a7bf3f167301a3ba1cea515e"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "memoffset"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mimalloc"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e2894987a3459f3ffb755608bd82188f8ed00d0ae077f1edea29c068d639d98"
+dependencies = [
+ "libmimalloc-sys",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-sys",
+]
+
+[[package]]
+name = "pendulum"
+version = "3.0.0-alpha-1"
+dependencies = [
+ "mimalloc",
+ "pyo3",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "pyo3"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffb88ae05f306b4bfcde40ac4a51dc0b05936a9207a4b75b798c7729c4258a59"
+dependencies = [
+ "cfg-if",
+ "indoc",
+ "libc",
+ "memoffset",
+ "parking_lot",
+ "pyo3-build-config",
+ "pyo3-ffi",
+ "pyo3-macros",
+ "unindent",
+]
+
+[[package]]
+name = "pyo3-build-config"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "554db24f0b3c180a9c0b1268f91287ab3f17c162e15b54caaae5a6b3773396b0"
+dependencies = [
+ "once_cell",
+ "python3-dll-a",
+ "target-lexicon",
+]
+
+[[package]]
+name = "pyo3-ffi"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "922ede8759e8600ad4da3195ae41259654b9c55da4f7eec84a0ccc7d067a70a4"
+dependencies = [
+ "libc",
+ "pyo3-build-config",
+]
+
+[[package]]
+name = "pyo3-macros"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a5caec6a1dd355964a841fcbeeb1b89fe4146c87295573f94228911af3cc5a2"
+dependencies = [
+ "proc-macro2",
+ "pyo3-macros-backend",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pyo3-macros-backend"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0b78ccbb160db1556cdb6fd96c50334c5d4ec44dc5e0a968d0a1208fa0efa8b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "python3-dll-a"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f07cd4412be8fa09a721d40007c483981bbe072cd6a21f2e83e04ec8f8343f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "smallvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae9980cab1db3fceee2f6c6f643d5d8de2997c58ee8d25fb0cc8a9e9e7348e5"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+
+[[package]]
+name = "unindent"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c"
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..0c3176a
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "pendulum"
+version = "3.0.0-alpha-1"
+edition = "2021"
+
+[lib]
+name = "_pendulum"
+crate-type = ["cdylib", "rlib"]
+path = "rust/lib.rs"
+
+[profile.release]
+lto = "fat"
+codegen-units = 1
+strip = true
+
+[dependencies]
+pyo3 = { version = "0.19.0", features = ["extension-module", "generate-import-lib"] }
+mimalloc = { version = "0.1.30", optional = true, default-features = false }
+
+[features]
+extension-module = ["pyo3/extension-module"]
+default = ["mimalloc"]
diff --git a/Makefile b/Makefile
index e68b94c..468cd9a 100644
--- a/Makefile
+++ b/Makefile
@@ -51,3 +51,13 @@ build_wheels_i686:
 # run tests against all supported python versions
 tox:
 	@tox
+
+
+lint-rust:
+	cargo fmt --all -- --check
+	cargo clippy --tests -- -D warnings
+
+
+format-rust:
+	cargo fmt --all
+	cargo clippy --tests --fix --allow-dirty -- -D warnings
diff --git a/pendulum/_extensions/_helpers.pyi b/_pendulum.pyi
similarity index 66%
rename from pendulum/_extensions/_helpers.pyi
rename to _pendulum.pyi
index f776355..74d7d83 100644
--- a/pendulum/_extensions/_helpers.pyi
+++ b/_pendulum.pyi
@@ -2,14 +2,20 @@ from __future__ import annotations
 
 from datetime import date
 from datetime import datetime
+from datetime import time
 from typing import NamedTuple
 
-def days_in_year(year: int) -> int: ...
-def is_leap(year: int) -> bool: ...
-def is_long_year(year: int) -> bool: ...
-def local_time(
-    unix_time: int, utc_offset: int, microseconds: int
-) -> tuple[int, int, int, int, int, int, int]: ...
+class Duration:
+    years: int = 0
+    months: int = 0
+    weeks: int = 0
+    days: int = 0
+    remaining_days: int = 0
+    hours: int = 0
+    minutes: int = 0
+    seconds: int = 0
+    remaining_seconds: int = 0
+    microseconds: int = 0
 
 class PreciseDiff(NamedTuple):
     years: int
@@ -21,6 +27,14 @@ class PreciseDiff(NamedTuple):
     microseconds: int
     total_days: int
 
+def parse_iso8601(
+    text: str,
+) -> datetime | date | time | Duration: ...
+def days_in_year(year: int) -> int: ...
+def is_leap(year: int) -> bool: ...
+def is_long_year(year: int) -> bool: ...
+def local_time(
+    unix_time: int, utc_offset: int, microseconds: int
+) -> tuple[int, int, int, int, int, int, int]: ...
 def precise_diff(d1: datetime | date, d2: datetime | date) -> PreciseDiff: ...
-def timestamp(dt: datetime) -> int: ...
 def week_day(year: int, month: int, day: int) -> int: ...
diff --git a/build.py b/build.py
index 8a72beb..60c880c 100644
--- a/build.py
+++ b/build.py
@@ -1,19 +1,41 @@
+import os
+import shlex
+import shutil
 import subprocess
+import zipfile
 
 from pathlib import Path
 
 
-def meson(*args):
-    subprocess.call(["meson", *list(args)])
+def maturin(*args):
+    subprocess.call(["maturin", *list(args)])
 
 
 def _build():
     build_dir = Path(__file__).parent.joinpath("build")
     build_dir.mkdir(parents=True, exist_ok=True)
 
-    meson("setup", build_dir.as_posix())
-    meson("compile", "-C", build_dir.as_posix())
-    meson("install", "-C", build_dir.as_posix())
+    wheels_dir = Path(__file__).parent.joinpath("target/wheels")
+    if wheels_dir.exists():
+        shutil.rmtree(wheels_dir)
+
+    cargo_args = []
+    if os.getenv("MATURIN_BUILD_ARGS"):
+        cargo_args = shlex.split(os.getenv("MATURIN_BUILD_ARGS", ""))
+
+    maturin("build", "-r", *cargo_args)
+
+    # We won't use the wheel built by maturin directly since
+    # we want Poetry to build it but we need to retrieve the
+    # compiled extensions from the maturin wheel.
+    wheel = list(wheels_dir.glob("*.whl"))[0]
+    with zipfile.ZipFile(wheel.as_posix()) as whl:
+        whl.extractall(wheels_dir.as_posix())
+
+        for extension in wheels_dir.rglob("**/*.so"):
+            shutil.copyfile(extension, Path(__file__).parent.joinpath(extension.name))
+
+    shutil.rmtree(wheels_dir)
 
 
 def build(setup_kwargs):
@@ -22,11 +44,12 @@ def build(setup_kwargs):
     """
     try:
         _build()
-    except Exception:
+    except Exception as e:
         print(
-            "  Unable to build C extensions, "
+            "  Unable to build Rust extensions, "
             "Pendulum will use the pure python version of the extensions."
         )
+        print(e)
 
 
 if __name__ == "__main__":
diff --git a/meson.build b/meson.build
deleted file mode 100644
index 666c281..0000000
--- a/meson.build
+++ /dev/null
@@ -1,20 +0,0 @@
-project('pendulum C extensions', 'c')
-
-py_mod = import('python')
-py = py_mod.find_installation()
-py_dep = py.dependency()
-
-extensions = [
-  ['_helpers', 'pendulum/_extensions/_helpers.c', meson.source_root() / 'pendulum/_extensions/'],
-  ['_iso8601', 'pendulum/parsing/_iso8601.c', meson.source_root() / 'pendulum/parsing/'],
-]
-
-foreach extension : extensions
-  py.extension_module(
-    extension[0],
-    extension[1],
-    dependencies : py_dep,
-    install : true,
-    install_dir: extension[2]
-  )
-endforeach
diff --git a/pendulum/_extensions/__init__.py b/pendulum/_extensions/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/pendulum/_extensions/_helpers.c b/pendulum/_extensions/_helpers.c
deleted file mode 100644
index a3114d9..0000000
--- a/pendulum/_extensions/_helpers.c
+++ /dev/null
@@ -1,931 +0,0 @@
-/* ------------------------------------------------------------------------- */
-
-#include <Python.h>
-#include <datetime.h>
-#include <structmember.h>
-#include <math.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <time.h>
-
-/* ------------------------------------------------------------------------- */
-
-#define EPOCH_YEAR 1970
-
-#define DAYS_PER_N_YEAR 365
-#define DAYS_PER_L_YEAR 366
-
-#define USECS_PER_SEC 1000000
-
-#define SECS_PER_MIN 60
-#define SECS_PER_HOUR (60 * SECS_PER_MIN)
-#define SECS_PER_DAY (SECS_PER_HOUR * 24)
-
-// 400-year chunks always have 146097 days (20871 weeks).
-#define DAYS_PER_400_YEARS 146097L
-#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)SECS_PER_DAY)
-
-// The number of seconds in an aligned 100-year chunk, for those that
-// do not begin with a leap year and those that do respectively.
-const int64_t SECS_PER_100_YEARS[2] = {
-    (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY,
-    (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * DAYS_PER_L_YEAR) * SECS_PER_DAY};
-
-// The number of seconds in an aligned 4-year chunk, for those that
-// do not begin with a leap year and those that do respectively.
-const int32_t SECS_PER_4_YEARS[2] = {
-    (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY,
-    (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY};
-
-// The number of seconds in non-leap and leap years respectively.
-const int32_t SECS_PER_YEAR[2] = {
-    DAYS_PER_N_YEAR * SECS_PER_DAY,
-    DAYS_PER_L_YEAR *SECS_PER_DAY};
-
-#define MONTHS_PER_YEAR 12
-
-// The month lengths in non-leap and leap years respectively.
-const int32_t DAYS_PER_MONTHS[2][13] = {
-    {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
-    {-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};
-
-// The day offsets of the beginning of each (1-based) month in non-leap
-// and leap years respectively.
-// For example, in a leap year there are 335 days before December.
-const int32_t MONTHS_OFFSETS[2][14] = {
-    {-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365},
-    {-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}};
-
-const int DAY_OF_WEEK_TABLE[12] = {
-    0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
-
-#define TM_SUNDAY 0
-#define TM_MONDAY 1
-#define TM_TUESDAY 2
-#define TM_WEDNESDAY 3
-#define TM_THURSDAY 4
-#define TM_FRIDAY 5
-#define TM_SATURDAY 6
-
-#define TM_JANUARY 0
-#define TM_FEBRUARY 1
-#define TM_MARCH 2
-#define TM_APRIL 3
-#define TM_MAY 4
-#define TM_JUNE 5
-#define TM_JULY 6
-#define TM_AUGUST 7
-#define TM_SEPTEMBER 8
-#define TM_OCTOBER 9
-#define TM_NOVEMBER 10
-#define TM_DECEMBER 11
-
-/* ------------------------------------------------------------------------- */
-
-int _p(int y)
-{
-    return y + y / 4 - y / 100 + y / 400;
-}
-
-int _is_leap(int year)
-{
-    return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
-}
-
-int _is_long_year(int year)
-{
-    return (_p(year) % 7 == 4) || (_p(year - 1) % 7 == 3);
-}
-
-int _week_day(int year, int month, int day)
-{
-    int y;
-    int w;
-
-    y = year - (month < 3);
-
-    w = (_p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7;
-
-    if (!w)
-    {
-        w = 7;
-    }
-
-    return w;
-}
-
-int _days_in_year(int year)
-{
-    if (_is_leap(year))
-    {
-        return DAYS_PER_L_YEAR;
-    }
-
-    return DAYS_PER_N_YEAR;
-}
-
-int _day_number(int year, int month, int day)
-{
-    month = (month + 9) % 12;
-    year = year - month / 10;
-
-    return (
-        365 * year + year / 4 - year / 100 + year / 400 + (month * 306 + 5) / 10 + (day - 1));
-}
-
-int _get_offset(PyObject *dt)
-{
-    PyObject *tzinfo;
-    PyObject *offset;
-
-    tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo;
-
-    if (tzinfo != Py_None)
-    {
-        offset = PyObject_CallMethod(tzinfo, "utcoffset", "O", dt);
-
-        return PyDateTime_DELTA_GET_DAYS(offset) * SECS_PER_DAY + PyDateTime_DELTA_GET_SECONDS(offset);
-    }
-
-    return 0;
-}
-
-int _has_tzinfo(PyObject *dt)
-{
-    return ((_PyDateTime_BaseTZInfo *)(dt))->hastzinfo;
-}
-
-char *_get_tz_name(PyObject *dt)
-{
-    PyObject *tzinfo;
-    char *tz = "";
-
-    tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo;
-
-    if (tzinfo != Py_None)
-    {
-        if (PyObject_HasAttrString(tzinfo, "key"))
-        {
-            // zoneinfo timezone
-            tz = (char *)PyUnicode_AsUTF8(
-                PyObject_GetAttrString(tzinfo, "name"));
-        }
-        else if (PyObject_HasAttrString(tzinfo, "name"))
-        {
-            // Pendulum timezone
-            tz = (char *)PyUnicode_AsUTF8(
-                PyObject_GetAttrString(tzinfo, "name"));
-        }
-        else if (PyObject_HasAttrString(tzinfo, "zone"))
-        {
-            // pytz timezone
-            tz = (char *)PyUnicode_AsUTF8(
-                PyObject_GetAttrString(tzinfo, "zone"));
-        }
-    }
-
-    return tz;
-}
-
-/* ------------------------ Custom Types ------------------------------- */
-
-/*
- * class Diff():
- */
-typedef struct
-{
-    PyObject_HEAD int years;
-    int months;
-    int days;
-    int hours;
-    int minutes;
-    int seconds;
-    int microseconds;
-    int total_days;
-} Diff;
-
-/*
- * def __init__(self, years, months, days, hours, minutes, seconds, microseconds, total_days):
- *     self.years = years
- *     # ...
-*/
-static int Diff_init(Diff *self, PyObject *args, PyObject *kwargs)
-{
-    int years;
-    int months;
-    int days;
-    int hours;
-    int minutes;
-    int seconds;
-    int microseconds;
-    int total_days;
-
-    if (!PyArg_ParseTuple(args, "iiiiiii", &years, &months, &days, &hours, &minutes, &seconds, &microseconds, &total_days))
-        return -1;
-
-    self->years = years;
-    self->months = months;
-    self->days = days;
-    self->hours = hours;
-    self->minutes = minutes;
-    self->seconds = seconds;
-    self->microseconds = microseconds;
-    self->total_days = total_days;
-
-    return 0;
-}
-
-/*
- * def __repr__(self):
- *     return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format(
- *         self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds
- *     )
- */
-static PyObject *Diff_repr(Diff *self)
-{
-    return PyUnicode_FromFormat(
-        "%d years %d months %d days %d hours %d minutes %d seconds %d microseconds",
-        self->years,
-        self->months,
-        self->days,
-        self->hours,
-        self->minutes,
-        self->seconds,
-        self->microseconds);
-}
-
-/*
- * Instantiate new Diff_type object
- * Skip overhead of calling PyObject_New and PyObject_Init.
- * Directly allocate object.
- */
-static PyObject *new_diff_ex(int years, int months, int days, int hours, int minutes, int seconds, int microseconds, int total_days, PyTypeObject *type)
-{
-    Diff *self = (Diff *)(type->tp_alloc(type, 0));
-
-    if (self != NULL)
-    {
-        self->years = years;
-        self->months = months;
-        self->days = days;
-        self->hours = hours;
-        self->minutes = minutes;
-        self->seconds = seconds;
-        self->microseconds = microseconds;
-        self->total_days = total_days;
-    }
-
-    return (PyObject *)self;
-}
-
-/*
- * Class member / class attributes
- */
-static PyMemberDef Diff_members[] = {
-    {"years", T_INT, offsetof(Diff, years), 0, "years in diff"},
-    {"months", T_INT, offsetof(Diff, months), 0, "months in diff"},
-    {"days", T_INT, offsetof(Diff, days), 0, "days in diff"},
-    {"hours", T_INT, offsetof(Diff, hours), 0, "hours in diff"},
-    {"minutes", T_INT, offsetof(Diff, minutes), 0, "minutes in diff"},
-    {"seconds", T_INT, offsetof(Diff, seconds), 0, "seconds in diff"},
-    {"microseconds", T_INT, offsetof(Diff, microseconds), 0, "microseconds in diff"},
-    {"total_days", T_INT, offsetof(Diff, total_days), 0, "total days in diff"},
-    {NULL}};
-
-static PyTypeObject Diff_type = {
-    PyVarObject_HEAD_INIT(NULL, 0) "PreciseDiff",      /* tp_name */
-    sizeof(Diff),                                      /* tp_basicsize */
-    0,                                                 /* tp_itemsize */
-    0,                                                 /* tp_dealloc */
-    0,                                                 /* tp_print */
-    0,                                                 /* tp_getattr */
-    0,                                                 /* tp_setattr */
-    0,                                                 /* tp_as_async */
-    (reprfunc)Diff_repr,                               /* tp_repr */
-    0,                                                 /* tp_as_number */
-    0,                                                 /* tp_as_sequence */
-    0,                                                 /* tp_as_mapping */
-    0,                                                 /* tp_hash  */
-    0,                                                 /* tp_call */
-    (reprfunc)Diff_repr,                               /* tp_str */
-    0,                                                 /* tp_getattro */
-    0,                                                 /* tp_setattro */
-    0,                                                 /* tp_as_buffer */
-    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,          /* tp_flags */
-    "Precise difference between two datetime objects", /* tp_doc */
-};
-
-#define new_diff(years, months, days, hours, minutes, seconds, microseconds, total_days) new_diff_ex(years, months, days, hours, minutes, seconds, microseconds, total_days, &Diff_type)
-
-/* -------------------------- Functions --------------------------*/
-
-PyObject *is_leap(PyObject *self, PyObject *args)
-{
-    PyObject *leap;
-    int year;
-
-    if (!PyArg_ParseTuple(args, "i", &year))
-    {
-        PyErr_SetString(
-            PyExc_ValueError, "Invalid parameters");
-        return NULL;
-    }
-
-    leap = PyBool_FromLong(_is_leap(year));
-
-    return leap;
-}
-
-PyObject *is_long_year(PyObject *self, PyObject *args)
-{
-    PyObject *is_long;
-    int year;
-
-    if (!PyArg_ParseTuple(args, "i", &year))
-    {
-        PyErr_SetString(
-            PyExc_ValueError, "Invalid parameters");
-        return NULL;
-    }
-
-    is_long = PyBool_FromLong(_is_long_year(year));
-
-    return is_long;
-}
-
-PyObject *week_day(PyObject *self, PyObject *args)
-{
-    PyObject *wd;
-    int year;
-    int month;
-    int day;
-
-    if (!PyArg_ParseTuple(args, "iii", &year, &month, &day))
-    {
-        PyErr_SetString(
-            PyExc_ValueError, "Invalid parameters");
-        return NULL;
-    }
-
-    wd = PyLong_FromLong(_week_day(year, month, day));
-
-    return wd;
-}
-
-PyObject *days_in_year(PyObject *self, PyObject *args)
-{
-    PyObject *ndays;
-    int year;
-
-    if (!PyArg_ParseTuple(args, "i", &year))
-    {
-        PyErr_SetString(
-            PyExc_ValueError, "Invalid parameters");
-        return NULL;
-    }
-
-    ndays = PyLong_FromLong(_days_in_year(year));
-
-    return ndays;
-}
-
-PyObject *timestamp(PyObject *self, PyObject *args)
-{
-    int64_t result;
-    PyObject *dt;
-
-    if (!PyArg_ParseTuple(args, "O", &dt))
-    {
-        PyErr_SetString(
-            PyExc_ValueError, "Invalid parameters");
-        return NULL;
-    }
-
-    int year = (double)PyDateTime_GET_YEAR(dt);
-    int month = PyDateTime_GET_MONTH(dt);
-    int day = PyDateTime_GET_DAY(dt);
-    int hour = PyDateTime_DATE_GET_HOUR(dt);
-    int minute = PyDateTime_DATE_GET_MINUTE(dt);
-    int second = PyDateTime_DATE_GET_SECOND(dt);
-
-    result = (year - 1970) * 365 + MONTHS_OFFSETS[0][month];
-    result += (int)floor((double)(year - 1968) / 4);
-    result -= (year - 1900) / 100;
-    result += (year - 1600) / 400;
-
-    if (_is_leap(year) && month < 3)
-    {
-        result -= 1;
-    }
-
-    result += day - 1;
-    result *= 24;
-    result += hour;
-    result *= 60;
-    result += minute;
-    result *= 60;
-    result += second;
-
-    return PyLong_FromSsize_t(result);
-}
-
-PyObject *local_time(PyObject *self, PyObject *args)
-{
-    double unix_time;
-    int32_t utc_offset;
-    int32_t year;
-    int32_t microsecond;
-    int64_t seconds;
-    int32_t leap_year;
-    int64_t sec_per_100years;
-    int64_t sec_per_4years;
-    int32_t sec_per_year;
-    int32_t month;
-    int32_t day;
-    int32_t month_offset;
-    int32_t hour;
-    int32_t minute;
-    int32_t second;
-
-    if (!PyArg_ParseTuple(args, "dii", &unix_time, &utc_offset, &microsecond))
-    {
-        PyErr_SetString(
-            PyExc_ValueError, "Invalid parameters");
-        return NULL;
-    }
-
-    year = EPOCH_YEAR;
-    seconds = (int64_t)floor(unix_time);
-
-    // Shift to a base year that is 400-year aligned.
-    if (seconds >= 0)
-    {
-        seconds -= 10957L * SECS_PER_DAY;
-        year += 30; // == 2000;
-    }
-    else
-    {
-        seconds += (int64_t)(146097L - 10957L) * SECS_PER_DAY;
-        year -= 370; // == 1600;
-    }
-
-    seconds += utc_offset;
-
-    // Handle years in chunks of 400/100/4/1
-    year += 400 * (seconds / SECS_PER_400_YEARS);
-    seconds %= SECS_PER_400_YEARS;
-    if (seconds < 0)
-    {
-        seconds += SECS_PER_400_YEARS;
-        year -= 400;
-    }
-
-    leap_year = 1; // 4-century aligned
-
-    sec_per_100years = SECS_PER_100_YEARS[leap_year];
-
-    while (seconds >= sec_per_100years)
-    {
-        seconds -= sec_per_100years;
-        year += 100;
-        leap_year = 0; // 1-century, non 4-century aligned
-        sec_per_100years = SECS_PER_100_YEARS[leap_year];
-    }
-
-    sec_per_4years = SECS_PER_4_YEARS[leap_year];
-    while (seconds >= sec_per_4years)
-    {
-        seconds -= sec_per_4years;
-        year += 4;
-        leap_year = 1; // 4-year, non century aligned
-        sec_per_4years = SECS_PER_4_YEARS[leap_year];
-    }
-
-    sec_per_year = SECS_PER_YEAR[leap_year];
-    while (seconds >= sec_per_year)
-    {
-        seconds -= sec_per_year;
-        year += 1;
-        leap_year = 0; // non 4-year aligned
-        sec_per_year = SECS_PER_YEAR[leap_year];
-    }
-
-    // Handle months and days
-    month = TM_DECEMBER + 1;
-    day = seconds / SECS_PER_DAY + 1;
-    seconds %= SECS_PER_DAY;
-    while (month != TM_JANUARY + 1)
-    {
-        month_offset = MONTHS_OFFSETS[leap_year][month];
-        if (day > month_offset)
-        {
-            day -= month_offset;
-            break;
-        }
-
-        month -= 1;
-    }
-
-    // Handle hours, minutes and seconds
-    hour = seconds / SECS_PER_HOUR;
-    seconds %= SECS_PER_HOUR;
-    minute = seconds / SECS_PER_MIN;
-    second = seconds % SECS_PER_MIN;
-
-    return Py_BuildValue("NNNNNNN",
-                         PyLong_FromLong(year),
-                         PyLong_FromLong(month),
-                         PyLong_FromLong(day),
-                         PyLong_FromLong(hour),
-                         PyLong_FromLong(minute),
-                         PyLong_FromLong(second),
-                         PyLong_FromLong(microsecond));
-}
-
-// Calculate a precise difference between two datetimes.
-PyObject *precise_diff(PyObject *self, PyObject *args)
-{
-    PyObject *dt1;
-    PyObject *dt2;
-
-    if (!PyArg_ParseTuple(args, "OO", &dt1, &dt2))
-    {
-        PyErr_SetString(
-            PyExc_ValueError, "Invalid parameters");
-        return NULL;
-    }
-
-    int year_diff = 0;
-    int month_diff = 0;
-    int day_diff = 0;
-    int hour_diff = 0;
-    int minute_diff = 0;
-    int second_diff = 0;
-    int microsecond_diff = 0;
-    int sign = 1;
-    int year;
-    int month;
-    int leap;
-    int days_in_last_month;
-    int days_in_month;
-    int dt1_year = PyDateTime_GET_YEAR(dt1);
-    int dt2_year = PyDateTime_GET_YEAR(dt2);
-    int dt1_month = PyDateTime_GET_MONTH(dt1);
-    int dt2_month = PyDateTime_GET_MONTH(dt2);
-    int dt1_day = PyDateTime_GET_DAY(dt1);
-    int dt2_day = PyDateTime_GET_DAY(dt2);
-    int dt1_hour = 0;
-    int dt2_hour = 0;
-    int dt1_minute = 0;
-    int dt2_minute = 0;
-    int dt1_second = 0;
-    int dt2_second = 0;
-    int dt1_microsecond = 0;
-    int dt2_microsecond = 0;
-    int dt1_total_seconds = 0;
-    int dt2_total_seconds = 0;
-    int dt1_offset = 0;
-    int dt2_offset = 0;
-    int dt1_is_datetime = PyDateTime_Check(dt1);
-    int dt2_is_datetime = PyDateTime_Check(dt2);
-    char *tz1 = "";
-    char *tz2 = "";
-    int in_same_tz = 0;
-    int total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day));
-
-    // If both dates are datetimes, we check
-    // If we are in the same timezone
-    if (dt1_is_datetime && dt2_is_datetime)
-    {
-        if (_has_tzinfo(dt1))
-        {
-            tz1 = _get_tz_name(dt1);
-            dt1_offset = _get_offset(dt1);
-        }
-
-        if (_has_tzinfo(dt2))
-        {
-            tz2 = _get_tz_name(dt2);
-            dt2_offset = _get_offset(dt2);
-        }
-
-        in_same_tz = tz1 == tz2 && strncmp(tz1, "", 1);
-    }
-
-    // If we have datetimes (and not only dates)
-    // we get the information we need
-    if (dt1_is_datetime)
-    {
-        dt1_hour = PyDateTime_DATE_GET_HOUR(dt1);
-        dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1);
-        dt1_second = PyDateTime_DATE_GET_SECOND(dt1);
-        dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1);
-
-        if ((!in_same_tz && dt1_offset != 0) || total_days == 0)
-        {
-            dt1_hour -= dt1_offset / SECS_PER_HOUR;
-            dt1_offset %= SECS_PER_HOUR;
-            dt1_minute -= dt1_offset / SECS_PER_MIN;
-            dt1_offset %= SECS_PER_MIN;
-            dt1_second -= dt1_offset;
-
-            if (dt1_second < 0)
-            {
-                dt1_second += 60;
-                dt1_minute -= 1;
-            }
-            else if (dt1_second > 60)
-            {
-                dt1_second -= 60;
-                dt1_minute += 1;
-            }
-
-            if (dt1_minute < 0)
-            {
-                dt1_minute += 60;
-                dt1_hour -= 1;
-            }
-            else if (dt1_minute > 60)
-            {
-                dt1_minute -= 60;
-                dt1_hour += 1;
-            }
-
-            if (dt1_hour < 0)
-            {
-                dt1_hour += 24;
-                dt1_day -= 1;
-            }
-            else if (dt1_hour > 24)
-            {
-                dt1_hour -= 24;
-                dt1_day += 1;
-            }
-        }
-
-        dt1_total_seconds = (dt1_hour * SECS_PER_HOUR + dt1_minute * SECS_PER_MIN + dt1_second);
-    }
-
-    if (dt2_is_datetime)
-    {
-        dt2_hour = PyDateTime_DATE_GET_HOUR(dt2);
-        dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2);
-        dt2_second = PyDateTime_DATE_GET_SECOND(dt2);
-        dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2);
-
-        if ((!in_same_tz && dt2_offset != 0) || total_days == 0)
-        {
-            dt2_hour -= dt2_offset / SECS_PER_HOUR;
-            dt2_offset %= SECS_PER_HOUR;
-            dt2_minute -= dt2_offset / SECS_PER_MIN;
-            dt2_offset %= SECS_PER_MIN;
-            dt2_second -= dt2_offset;
-
-            if (dt2_second < 0)
-            {
-                dt2_second += 60;
-                dt2_minute -= 1;
-            }
-            else if (dt2_second > 60)
-            {
-                dt2_second -= 60;
-                dt2_minute += 1;
-            }
-
-            if (dt2_minute < 0)
-            {
-                dt2_minute += 60;
-                dt2_hour -= 1;
-            }
-            else if (dt2_minute > 60)
-            {
-                dt2_minute -= 60;
-                dt2_hour += 1;
-            }
-
-            if (dt2_hour < 0)
-            {
-                dt2_hour += 24;
-                dt2_day -= 1;
-            }
-            else if (dt2_hour > 24)
-            {
-                dt2_hour -= 24;
-                dt2_day += 1;
-            }
-        }
-
-        dt2_total_seconds = (dt2_hour * SECS_PER_HOUR + dt2_minute * SECS_PER_MIN + dt2_second);
-    }
-
-    // Direct comparison between two datetimes does not work
-    // so we need to check by properties
-    int dt1_gt_dt2 = (dt1_year > dt2_year || (dt1_year == dt2_year && dt1_month > dt2_month) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day > dt2_day) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds > dt2_total_seconds) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds == dt2_total_seconds && dt1_microsecond > dt2_microsecond));
-
-    if (dt1_gt_dt2)
-    {
-        PyObject *temp;
-        temp = dt1;
-        dt1 = dt2;
-        dt2 = temp;
-        sign = -1;
-
-        // Retrieving properties
-        dt1_year = PyDateTime_GET_YEAR(dt1);
-        dt2_year = PyDateTime_GET_YEAR(dt2);
-        dt1_month = PyDateTime_GET_MONTH(dt1);
-        dt2_month = PyDateTime_GET_MONTH(dt2);
-        dt1_day = PyDateTime_GET_DAY(dt1);
-        dt2_day = PyDateTime_GET_DAY(dt2);
-
-        if (dt2_is_datetime)
-        {
-            dt1_hour = PyDateTime_DATE_GET_HOUR(dt1);
-            dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1);
-            dt1_second = PyDateTime_DATE_GET_SECOND(dt1);
-            dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1);
-        }
-
-        if (dt1_is_datetime)
-        {
-            dt2_hour = PyDateTime_DATE_GET_HOUR(dt2);
-            dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2);
-            dt2_second = PyDateTime_DATE_GET_SECOND(dt2);
-            dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2);
-        }
-
-        total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day));
-    }
-
-    year_diff = dt2_year - dt1_year;
-    month_diff = dt2_month - dt1_month;
-    day_diff = dt2_day - dt1_day;
-    hour_diff = dt2_hour - dt1_hour;
-    minute_diff = dt2_minute - dt1_minute;
-    second_diff = dt2_second - dt1_second;
-    microsecond_diff = dt2_microsecond - dt1_microsecond;
-
-    if (microsecond_diff < 0)
-    {
-        microsecond_diff += 1e6;
-        second_diff -= 1;
-    }
-
-    if (second_diff < 0)
-    {
-        second_diff += 60;
-        minute_diff -= 1;
-    }
-
-    if (minute_diff < 0)
-    {
-        minute_diff += 60;
-        hour_diff -= 1;
-    }
-
-    if (hour_diff < 0)
-    {
-        hour_diff += 24;
-        day_diff -= 1;
-    }
-
-    if (day_diff < 0)
-    {
-        // If we have a difference in days,
-        // we have to check if they represent months
-        year = dt2_year;
-        month = dt2_month;
-
-        if (month == 1)
-        {
-            month = 12;
-            year -= 1;
-        }
-        else
-        {
-            month -= 1;
-        }
-
-        leap = _is_leap(year);
-
-        days_in_last_month = DAYS_PER_MONTHS[leap][month];
-        days_in_month = DAYS_PER_MONTHS[_is_leap(dt2_year)][dt2_month];
-
-        if (day_diff < days_in_month - days_in_last_month)
-        {
-            // We don't have a full month, we calculate days
-            if (days_in_last_month < dt1_day)
-            {
-                day_diff += dt1_day;
-            }
-            else
-            {
-                day_diff += days_in_last_month;
-            }
-        }
-        else if (day_diff == days_in_month - days_in_last_month)
-        {
-            // We have exactly a full month
-            // We remove the days difference
-            // and add one to the months difference
-            day_diff = 0;
-            month_diff += 1;
-        }
-        else
-        {
-            // We have a full month
-            day_diff += days_in_last_month;
-        }
-
-        month_diff -= 1;
-    }
-
-    if (month_diff < 0)
-    {
-        month_diff += 12;
-        year_diff -= 1;
-    }
-
-    return new_diff(
-        year_diff * sign,
-        month_diff * sign,
-        day_diff * sign,
-        hour_diff * sign,
-        minute_diff * sign,
-        second_diff * sign,
-        microsecond_diff * sign,
-        total_days * sign);
-}
-
-/* ------------------------------------------------------------------------- */
-
-static PyMethodDef helpers_methods[] = {
-    {"is_leap",
-     (PyCFunction)is_leap,
-     METH_VARARGS,
-     PyDoc_STR("Checks if a year is a leap year.")},
-    {"is_long_year",
-     (PyCFunction)is_long_year,
-     METH_VARARGS,
-     PyDoc_STR("Checks if a year is a long year.")},
-    {"week_day",
-     (PyCFunction)week_day,
-     METH_VARARGS,
-     PyDoc_STR("Returns the weekday number.")},
-    {"days_in_year",
-     (PyCFunction)days_in_year,
-     METH_VARARGS,
-     PyDoc_STR("Returns the number of days in the given year.")},
-    {"timestamp",
-     (PyCFunction)timestamp,
-     METH_VARARGS,
-     PyDoc_STR("Returns the timestamp of the given datetime.")},
-    {"local_time",
-     (PyCFunction)local_time,
-     METH_VARARGS,
-     PyDoc_STR("Returns a UNIX time as a broken down time for a particular transition type.")},
-    {"precise_diff",
-     (PyCFunction)precise_diff,
-     METH_VARARGS,
-     PyDoc_STR("Calculate a precise difference between two datetimes.")},
-    {NULL}};
-
-/* ------------------------------------------------------------------------- */
-
-static struct PyModuleDef moduledef = {
-    PyModuleDef_HEAD_INIT,
-    "_helpers",
-    NULL,
-    -1,
-    helpers_methods,
-    NULL,
-    NULL,
-    NULL,
-    NULL,
-};
-
-PyMODINIT_FUNC
-PyInit__helpers(void)
-{
-    PyObject *module;
-
-    PyDateTime_IMPORT;
-
-    module = PyModule_Create(&moduledef);
-
-    if (module == NULL)
-        return NULL;
-
-    // Diff declaration
-    Diff_type.tp_new = PyType_GenericNew;
-    Diff_type.tp_members = Diff_members;
-    Diff_type.tp_init = (initproc)Diff_init;
-
-    if (PyType_Ready(&Diff_type) < 0)
-        return NULL;
-
-    PyModule_AddObject(module, "PreciseDiff", (PyObject *)&Diff_type);
-
-    return module;
-}
diff --git a/pendulum/_extensions/helpers.py b/pendulum/_helpers.py
similarity index 95%
rename from pendulum/_extensions/helpers.py
rename to pendulum/_helpers.py
index 4dd4c72..1b586d3 100644
--- a/pendulum/_extensions/helpers.py
+++ b/pendulum/_helpers.py
@@ -84,28 +84,6 @@ def days_in_year(year: int) -> int:
     return DAYS_PER_N_YEAR
 
 
-def timestamp(dt: datetime.datetime) -> int:
-    year = dt.year
-
-    result = (year - 1970) * 365 + MONTHS_OFFSETS[0][dt.month]
-    result += (year - 1968) // 4
-    result -= (year - 1900) // 100
-    result += (year - 1600) // 400
-
-    if is_leap(year) and dt.month < 3:
-        result -= 1
-
-    result += dt.day - 1
-    result *= 24
-    result += dt.hour
-    result *= 60
-    result += dt.minute
-    result *= 60
-    result += dt.second
-
-    return result
-
-
 def local_time(
     unix_time: int, utc_offset: int, microseconds: int
 ) -> tuple[int, int, int, int, int, int, int]:
diff --git a/pendulum/date.py b/pendulum/date.py
index 0fe776c..41d80e7 100644
--- a/pendulum/date.py
+++ b/pendulum/date.py
@@ -9,6 +9,7 @@ from datetime import date
 from datetime import datetime
 from datetime import timedelta
 from typing import TYPE_CHECKING
+from typing import ClassVar
 from typing import NoReturn
 from typing import cast
 from typing import overload
@@ -38,7 +39,7 @@ if TYPE_CHECKING:
 
 class Date(FormattableMixin, date):
     # Names of days of the week
-    _days = {
+    _days: ClassVar[dict[int, str]] = {
         SUNDAY: "Sunday",
         MONDAY: "Monday",
         TUESDAY: "Tuesday",
@@ -48,7 +49,14 @@ class Date(FormattableMixin, date):
         SATURDAY: "Saturday",
     }
 
-    _MODIFIERS_VALID_UNITS = ["day", "week", "month", "year", "decade", "century"]
+    _MODIFIERS_VALID_UNITS: ClassVar[list[str]] = [
+        "day",
+        "week",
+        "month",
+        "year",
+        "decade",
+        "century",
+    ]
 
     # Getters/Setters
 
diff --git a/pendulum/datetime.py b/pendulum/datetime.py
index 93f0ade..d036d25 100644
--- a/pendulum/datetime.py
+++ b/pendulum/datetime.py
@@ -3,6 +3,7 @@ from __future__ import annotations
 import calendar
 import datetime
 import sys
+import traceback
 
 from typing import TYPE_CHECKING
 from typing import Any
@@ -55,7 +56,7 @@ class DateTime(datetime.datetime, Date):
 
     # Formats
 
-    _FORMATS: dict[str, str | Callable[[datetime.datetime], str]] = {
+    _FORMATS: ClassVar[dict[str, str | Callable[[datetime.datetime], str]]] = {
         "atom": ATOM,
         "cookie": COOKIE,
         "iso8601": lambda dt: dt.isoformat(),
@@ -69,7 +70,7 @@ class DateTime(datetime.datetime, Date):
         "w3c": W3C,
     }
 
-    _MODIFIERS_VALID_UNITS: list[str] = [
+    _MODIFIERS_VALID_UNITS: ClassVar[list[str]] = [
         "second",
         "minute",
         "hour",
@@ -1218,9 +1219,7 @@ class DateTime(datetime.datetime, Date):
             # This is a workaround for Python 3.8+
             # since calling astimezone() will call this method
             # instead of the base datetime class one.
-            import inspect
-
-            caller = inspect.stack()[1][3]
+            caller = traceback.extract_stack(limit=2)[0].name
             if caller == "astimezone":
                 return super().__add__(other)
 
diff --git a/pendulum/formatting/formatter.py b/pendulum/formatting/formatter.py
index d1bbab7..d8d13f6 100644
--- a/pendulum/formatting/formatter.py
+++ b/pendulum/formatting/formatter.py
@@ -6,6 +6,7 @@ import re
 from typing import TYPE_CHECKING
 from typing import Any
 from typing import Callable
+from typing import ClassVar
 from typing import Match
 from typing import Sequence
 from typing import cast
@@ -66,7 +67,9 @@ class Formatter:
 
     _FROM_FORMAT_RE: re.Pattern[str] = re.compile(r"(?<!\\\[)" + _TOKENS + r"(?!\\\])")
 
-    _LOCALIZABLE_TOKENS: dict[str, str | Callable[[Locale], Sequence[str]] | None] = {
+    _LOCALIZABLE_TOKENS: ClassVar[
+        dict[str, str | Callable[[Locale], Sequence[str]] | None]
+    ] = {
         "Qo": None,
         "MMMM": "months.wide",
         "MMM": "months.abbreviated",
@@ -91,7 +94,7 @@ class Formatter:
         ),
     }
 
-    _TOKENS_RULES: dict[str, Callable[[pendulum.DateTime], str]] = {
+    _TOKENS_RULES: ClassVar[dict[str, Callable[[pendulum.DateTime], str]]] = {
         # Year
         "YYYY": lambda dt: f"{dt.year:d}",
         "YY": lambda dt: f"{dt.year:d}"[2:],
@@ -137,7 +140,7 @@ class Formatter:
         "z": lambda dt: f'{dt.timezone_name or ""}',
     }
 
-    _DATE_FORMATS = {
+    _DATE_FORMATS: ClassVar[dict[str, str]] = {
         "LTS": "formats.time.full",
         "LT": "formats.time.short",
         "L": "formats.date.short",
@@ -146,7 +149,7 @@ class Formatter:
         "LLLL": "formats.datetime.full",
     }
 
-    _DEFAULT_DATE_FORMATS = {
+    _DEFAULT_DATE_FORMATS: ClassVar[dict[str, str]] = {
         "LTS": "h:mm:ss A",
         "LT": "h:mm A",
         "L": "MM/DD/YYYY",
@@ -155,7 +158,7 @@ class Formatter:
         "LLLL": "dddd, MMMM D, YYYY h:mm A",
     }
 
-    _REGEX_TOKENS: dict[str, str | Sequence[str] | None] = {
+    _REGEX_TOKENS: ClassVar[dict[str, str | Sequence[str] | None]] = {
         "Y": _MATCH_SIGNED,
         "YY": (_MATCH_1_TO_2, _MATCH_2),
         "YYYY": (_MATCH_1_TO_4, _MATCH_4),
@@ -196,7 +199,7 @@ class Formatter:
         "z": _MATCH_TIMEZONE,
     }
 
-    _PARSE_TOKENS: dict[str, Callable[[str], Any]] = {
+    _PARSE_TOKENS: ClassVar[dict[str, Callable[[str], Any]]] = {
         "YYYY": lambda year: int(year),
         "YY": lambda year: int(year),
         "Q": lambda quarter: int(quarter),
@@ -547,7 +550,10 @@ class Formatter:
 
         if "Y" in token:
             if token == "YY":
-                parsed_token = now.year // 100 * 100 + parsed_token
+                if parsed_token <= 68:
+                    parsed_token += 2000
+                else:
+                    parsed_token += 1900
 
             parsed["year"] = parsed_token
         elif token == "Q":
diff --git a/pendulum/helpers.py b/pendulum/helpers.py
index 6b62fe7..e9390ca 100644
--- a/pendulum/helpers.py
+++ b/pendulum/helpers.py
@@ -31,23 +31,21 @@ try:
     if not with_extensions or struct.calcsize("P") == 4:
         raise ImportError()
 
-    from pendulum._extensions._helpers import PreciseDiff
-    from pendulum._extensions._helpers import days_in_year
-    from pendulum._extensions._helpers import is_leap
-    from pendulum._extensions._helpers import is_long_year
-    from pendulum._extensions._helpers import local_time
-    from pendulum._extensions._helpers import precise_diff
-    from pendulum._extensions._helpers import timestamp
-    from pendulum._extensions._helpers import week_day
+    from _pendulum import PreciseDiff
+    from _pendulum import days_in_year
+    from _pendulum import is_leap
+    from _pendulum import is_long_year
+    from _pendulum import local_time
+    from _pendulum import precise_diff
+    from _pendulum import week_day
 except ImportError:
-    from pendulum._extensions.helpers import PreciseDiff  # type: ignore[assignment]
-    from pendulum._extensions.helpers import days_in_year
-    from pendulum._extensions.helpers import is_leap
-    from pendulum._extensions.helpers import is_long_year
-    from pendulum._extensions.helpers import local_time
-    from pendulum._extensions.helpers import precise_diff  # type: ignore[assignment]
-    from pendulum._extensions.helpers import timestamp
-    from pendulum._extensions.helpers import week_day
+    from pendulum._helpers import PreciseDiff  # type: ignore[assignment]
+    from pendulum._helpers import days_in_year
+    from pendulum._helpers import is_leap
+    from pendulum._helpers import is_long_year
+    from pendulum._helpers import local_time
+    from pendulum._helpers import precise_diff  # type: ignore[assignment]
+    from pendulum._helpers import week_day
 
 difference_formatter = DifferenceFormatter()
 
@@ -211,7 +209,6 @@ __all__ = [
     "is_long_year",
     "local_time",
     "precise_diff",
-    "timestamp",
     "week_day",
     "add_duration",
     "format_diff",
diff --git a/pendulum/interval.py b/pendulum/interval.py
index 0c77305..2721494 100644
--- a/pendulum/interval.py
+++ b/pendulum/interval.py
@@ -448,3 +448,6 @@ class Interval(Duration):
             )
         else:
             return self.as_duration() == other
+
+    def __ne__(self, other: object) -> bool:
+        return not self.__eq__(other)
diff --git a/pendulum/parser.py b/pendulum/parser.py
index d39842f..2cb5ef9 100644
--- a/pendulum/parser.py
+++ b/pendulum/parser.py
@@ -5,6 +5,7 @@ import typing as t
 
 import pendulum
 
+from pendulum.duration import Duration
 from pendulum.parsing import _Interval
 from pendulum.parsing import parse as base_parse
 from pendulum.tz.timezone import UTC
@@ -13,14 +14,13 @@ from pendulum.tz.timezone import UTC
 if t.TYPE_CHECKING:
     from pendulum.date import Date
     from pendulum.datetime import DateTime
-    from pendulum.duration import Duration
     from pendulum.interval import Interval
     from pendulum.time import Time
 
 try:
-    from pendulum.parsing._iso8601 import Duration as CDuration
+    from _pendulum import Duration as RustDuration
 except ImportError:
-    CDuration = None  # type: ignore[misc, assignment]
+    RustDuration = None  # type: ignore[assignment,misc]
 
 
 def parse(text: str, **options: t.Any) -> Date | Time | DateTime | Duration:
@@ -110,7 +110,10 @@ def _parse(text: str, **options: t.Any) -> Date | DateTime | Time | Duration | I
             ),
         )
 
-    if CDuration and isinstance(parsed, CDuration):  # type: ignore[truthy-function]
+    if isinstance(parsed, Duration):
+        return parsed
+
+    if RustDuration is not None and isinstance(parsed, RustDuration):
         return pendulum.duration(
             years=parsed.years,
             months=parsed.months,
@@ -122,4 +125,4 @@ def _parse(text: str, **options: t.Any) -> Date | DateTime | Time | Duration | I
             microseconds=parsed.microseconds,
         )
 
-    return parsed
+    raise NotImplementedError
diff --git a/pendulum/parsing/__init__.py b/pendulum/parsing/__init__.py
index 3b64994..908b670 100644
--- a/pendulum/parsing/__init__.py
+++ b/pendulum/parsing/__init__.py
@@ -24,12 +24,13 @@ try:
     if not with_extensions or struct.calcsize("P") == 4:
         raise ImportError()
 
-    from pendulum.parsing._iso8601 import Duration
-    from pendulum.parsing._iso8601 import parse_iso8601
+    from _pendulum import Duration
+    from _pendulum import parse_iso8601
 except ImportError:
     from pendulum.duration import Duration  # type: ignore[assignment]
     from pendulum.parsing.iso8601 import parse_iso8601  # type: ignore[assignment]
 
+
 COMMON = re.compile(
     # Date (optional)  # noqa: ERA001
     "^"
diff --git a/pendulum/parsing/_iso8601.c b/pendulum/parsing/_iso8601.c
deleted file mode 100644
index 1322423..0000000
--- a/pendulum/parsing/_iso8601.c
+++ /dev/null
@@ -1,1361 +0,0 @@
-/* ------------------------------------------------------------------------- */
-
-#include <Python.h>
-#include <datetime.h>
-#include <structmember.h>
-#include <math.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <stdint.h>
-
-#ifndef PyVarObject_HEAD_INIT
-#define PyVarObject_HEAD_INIT(type, size) PyObject_HEAD_INIT(type) size,
-#endif
-
-
-/* ------------------------------------------------------------------------- */
-
-#define EPOCH_YEAR 1970
-
-#define DAYS_PER_N_YEAR 365
-#define DAYS_PER_L_YEAR 366
-
-#define USECS_PER_SEC 1000000
-
-#define SECS_PER_MIN 60
-#define SECS_PER_HOUR (60 * SECS_PER_MIN)
-#define SECS_PER_DAY (SECS_PER_HOUR * 24)
-
-// 400-year chunks always have 146097 days (20871 weeks).
-#define DAYS_PER_400_YEARS 146097L
-#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)SECS_PER_DAY)
-
-// The number of seconds in an aligned 100-year chunk, for those that
-// do not begin with a leap year and those that do respectively.
-const int64_t SECS_PER_100_YEARS[2] = {
-    (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY,
-    (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * DAYS_PER_L_YEAR) * SECS_PER_DAY
-};
-
-// The number of seconds in an aligned 4-year chunk, for those that
-// do not begin with a leap year and those that do respectively.
-const int32_t SECS_PER_4_YEARS[2] = {
-    (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY,
-    (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY
-};
-
-// The number of seconds in non-leap and leap years respectively.
-const int32_t SECS_PER_YEAR[2] = {
-    DAYS_PER_N_YEAR * SECS_PER_DAY,
-    DAYS_PER_L_YEAR * SECS_PER_DAY
-};
-
-#define MONTHS_PER_YEAR 12
-
-// The month lengths in non-leap and leap years respectively.
-const int32_t DAYS_PER_MONTHS[2][13] = {
-    {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
-    {-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
-};
-
-// The day offsets of the beginning of each (1-based) month in non-leap
-// and leap years respectively.
-// For example, in a leap year there are 335 days before December.
-const int32_t MONTHS_OFFSETS[2][14] = {
-    {-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365},
-    {-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}
-};
-
-const int DAY_OF_WEEK_TABLE[12] = {
-    0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4
-};
-
-#define TM_SUNDAY 0
-#define TM_MONDAY 1
-#define TM_TUESDAY 2
-#define TM_WEDNESDAY 3
-#define TM_THURSDAY 4
-#define TM_FRIDAY 5
-#define TM_SATURDAY 6
-
-#define TM_JANUARY 0
-#define TM_FEBRUARY 1
-#define TM_MARCH 2
-#define TM_APRIL 3
-#define TM_MAY 4
-#define TM_JUNE 5
-#define TM_JULY 6
-#define TM_AUGUST 7
-#define TM_SEPTEMBER 8
-#define TM_OCTOBER 9
-#define TM_NOVEMBER 10
-#define TM_DECEMBER 11
-
-// Parsing errors
-const int PARSER_INVALID_ISO8601 = 0;
-const int PARSER_INVALID_DATE = 1;
-const int PARSER_INVALID_TIME = 2;
-const int PARSER_INVALID_WEEK_DATE = 3;
-const int PARSER_INVALID_WEEK_NUMBER = 4;
-const int PARSER_INVALID_WEEKDAY_NUMBER = 5;
-const int PARSER_INVALID_ORDINAL_DAY_FOR_YEAR = 6;
-const int PARSER_INVALID_MONTH_OR_DAY = 7;
-const int PARSER_INVALID_MONTH = 8;
-const int PARSER_INVALID_DAY_FOR_MONTH = 9;
-const int PARSER_INVALID_HOUR = 10;
-const int PARSER_INVALID_MINUTE = 11;
-const int PARSER_INVALID_SECOND = 12;
-const int PARSER_INVALID_SUBSECOND = 13;
-const int PARSER_INVALID_TZ_OFFSET = 14;
-const int PARSER_INVALID_DURATION = 15;
-const int PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED = 16;
-
-const char PARSER_ERRORS[17][80] = {
-    "Invalid ISO 8601 string",
-    "Invalid date",
-    "Invalid time",
-    "Invalid week date",
-    "Invalid week number",
-    "Invalid weekday number",
-    "Invalid ordinal day for year",
-    "Invalid month and/or day",
-    "Invalid month",
-    "Invalid day for month",
-    "Invalid hour",
-    "Invalid minute",
-    "Invalid second",
-    "Invalid subsecond",
-    "Invalid timezone offset",
-    "Invalid duration",
-    "Float years and months are not supported"
-};
-
-/* ------------------------------------------------------------------------- */
-
-
-int p(int y) {
-    return y + y/4 - y/100 + y/400;
-}
-
-int is_leap(int year) {
-    return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
-}
-
-int week_day(int year, int month, int day) {
-    int y;
-    int w;
-
-    y = year - (month < 3);
-
-    w = (p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7;
-
-    if (!w) {
-        w = 7;
-    }
-
-    return w;
-}
-
-int days_in_year(int year) {
-    if (is_leap(year)) {
-        return DAYS_PER_L_YEAR;
-    }
-
-    return DAYS_PER_N_YEAR;
-}
-
-int is_long_year(int year) {
-    return (p(year) % 7 == 4) || (p(year - 1) % 7 == 3);
-}
-
-
-/* ------------------------ Custom Types ------------------------------- */
-
-
-/*
- * class FixedOffset(tzinfo):
- */
-typedef struct {
-    PyObject_HEAD
-    int offset;
-    char *tzname;
-} FixedOffset;
-
-/*
- * def __init__(self, offset):
- *     self.offset = offset
-*/
-static int FixedOffset_init(FixedOffset *self, PyObject *args, PyObject *kwargs) {
-    int offset;
-    char *tzname = NULL;
-
-    static char *kwlist[] = {"offset", "tzname", NULL};
-
-    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|s", kwlist, &offset, &tzname))
-        return -1;
-
-    self->offset = offset;
-    self->tzname = tzname;
-
-    return 0;
-}
-
-/*
- * def utcoffset(self, dt):
- *     return timedelta(seconds=self.offset * 60)
- */
-static PyObject *FixedOffset_utcoffset(FixedOffset *self, PyObject *args) {
-    return PyDelta_FromDSU(0, self->offset, 0);
-}
-
-/*
- * def dst(self, dt):
- *     return timedelta(seconds=self.offset * 60)
- */
-static PyObject *FixedOffset_dst(FixedOffset *self, PyObject *args) {
-    return PyDelta_FromDSU(0, self->offset, 0);
-}
-
-/*
- * def tzname(self, dt):
- *     sign = '+'
- *     if self.offset < 0:
- *         sign = '-'
- *     return f"{sign}{self.offset / 60}:{self.offset % 60}"
- */
-static PyObject *FixedOffset_tzname(FixedOffset *self, PyObject *args) {
-    if (self->tzname != NULL) {
-        return PyUnicode_FromString(self->tzname);
-    }
-
-    char sign = '+';
-    int offset = self->offset;
-
-    if (offset < 0) {
-        sign = '-';
-        offset *= -1;
-    }
-
-    return PyUnicode_FromFormat(
-        "%c%02d:%02d",
-        sign,
-        offset / SECS_PER_HOUR,
-        offset / SECS_PER_MIN % SECS_PER_MIN
-    );
-}
-
-/*
- * def __repr__(self):
- *     return self.tzname()
- */
-static PyObject *FixedOffset_repr(FixedOffset *self) {
-    return FixedOffset_tzname(self, NULL);
-}
-
-/*
- * Class member / class attributes
- */
-static PyMemberDef FixedOffset_members[] = {
-    {"offset", T_INT, offsetof(FixedOffset, offset), 0, "UTC offset"},
-    {NULL}
-};
-
-/*
- * Class methods
- */
-static PyMethodDef FixedOffset_methods[] = {
-    {"utcoffset", (PyCFunction)FixedOffset_utcoffset, METH_VARARGS, ""},
-    {"dst",       (PyCFunction)FixedOffset_dst,       METH_VARARGS, ""},
-    {"tzname",    (PyCFunction)FixedOffset_tzname,    METH_VARARGS, ""},
-    {NULL}
-};
-
-static PyTypeObject FixedOffset_type = {
-    PyVarObject_HEAD_INIT(NULL, 0)
-    "FixedOffset_type",             /* tp_name */
-    sizeof(FixedOffset),                    /* tp_basicsize */
-    0,                                      /* tp_itemsize */
-    0,                                      /* tp_dealloc */
-    0,                                      /* tp_print */
-    0,                                      /* tp_getattr */
-    0,                                      /* tp_setattr */
-    0,                                      /* tp_as_async */
-    (reprfunc)FixedOffset_repr,             /* tp_repr */
-    0,                                      /* tp_as_number */
-    0,                                      /* tp_as_sequence */
-    0,                                      /* tp_as_mapping */
-    0,                                      /* tp_hash  */
-    0,                                      /* tp_call */
-    (reprfunc)FixedOffset_repr,             /* tp_str */
-    0,                                      /* tp_getattro */
-    0,                                      /* tp_setattro */
-    0,                                      /* tp_as_buffer */
-    Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */
-    "TZInfo with fixed offset",             /* tp_doc */
-};
-
-/*
- * Instantiate new FixedOffset_type object
- * Skip overhead of calling PyObject_New and PyObject_Init.
- * Directly allocate object.
- */
-static PyObject *new_fixed_offset_ex(int offset, char *name, PyTypeObject *type) {
-    FixedOffset *self = (FixedOffset *) (type->tp_alloc(type, 0));
-
-    if (self != NULL) {
-        self->offset = offset;
-        self->tzname = name;
-    }
-
-    return (PyObject *) self;
-}
-
-#define new_fixed_offset(offset, name) new_fixed_offset_ex(offset, name, &FixedOffset_type)
-
-
-/*
- * class Duration():
- */
-typedef struct {
-    PyObject_HEAD
-    int years;
-    int months;
-    int weeks;
-    int days;
-    int hours;
-    int minutes;
-    int seconds;
-    int microseconds;
-} Duration;
-
-/*
- * def __init__(self, years, months, days, hours, minutes, seconds, microseconds):
- *     self.years = years
- *     # ...
-*/
-static int Duration_init(Duration *self, PyObject *args, PyObject *kwargs) {
-    int years;
-    int months;
-    int weeks;
-    int days;
-    int hours;
-    int minutes;
-    int seconds;
-    int microseconds;
-
-    if (!PyArg_ParseTuple(args, "iiiiiiii", &years, &months, &weeks, &days, &hours, &minutes, &seconds, &microseconds))
-        return -1;
-
-    self->years = years;
-    self->months = months;
-    self->weeks = weeks;
-    self->days = days;
-    self->hours = hours;
-    self->minutes = minutes;
-    self->seconds = seconds;
-    self->microseconds = microseconds;
-
-    return 0;
-}
-
-/*
- * def __repr__(self):
- *     return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format(
- *         self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds
- *     )
- */
-static PyObject *Duration_repr(Duration *self) {
-    return PyUnicode_FromFormat(
-        "%d years %d months %d weeks %d days %d hours %d minutes %d seconds %d microseconds",
-        self->years,
-        self->months,
-        self->weeks,
-        self->days,
-        self->hours,
-        self->minutes,
-        self->seconds,
-        self->microseconds
-    );
-}
-
-/*
- * Instantiate new Duration_type object
- * Skip overhead of calling PyObject_New and PyObject_Init.
- * Directly allocate object.
- */
-static PyObject *new_duration_ex(int years, int months, int weeks, int days, int hours, int minutes, int seconds, int microseconds, PyTypeObject *type) {
-    Duration *self = (Duration *) (type->tp_alloc(type, 0));
-
-    if (self != NULL) {
-        self->years = years;
-        self->months = months;
-        self->weeks = weeks;
-        self->days = days;
-        self->hours = hours;
-        self->minutes = minutes;
-        self->seconds = seconds;
-        self->microseconds = microseconds;
-    }
-
-    return (PyObject *) self;
-}
-
-/*
- * Class member / class attributes
- */
-static PyMemberDef Duration_members[] = {
-    {"years", T_INT, offsetof(Duration, years), 0, "years in duration"},
-    {"months", T_INT, offsetof(Duration, months), 0, "months in duration"},
-    {"weeks", T_INT, offsetof(Duration, weeks), 0, "weeks in duration"},
-    {"days", T_INT, offsetof(Duration, days), 0, "days in duration"},
-    {"remaining_days", T_INT, offsetof(Duration, days), 0, "days in duration"},
-    {"hours", T_INT, offsetof(Duration, hours), 0, "hours in duration"},
-    {"minutes", T_INT, offsetof(Duration, minutes), 0, "minutes in duration"},
-    {"seconds", T_INT, offsetof(Duration, seconds), 0, "seconds in duration"},
-    {"remaining_seconds", T_INT, offsetof(Duration, seconds), 0, "seconds in duration"},
-    {"microseconds", T_INT, offsetof(Duration, microseconds), 0, "microseconds in duration"},
-    {NULL}
-};
-
-static PyTypeObject Duration_type = {
-    PyVarObject_HEAD_INIT(NULL, 0)
-    "Duration",                             /* tp_name */
-    sizeof(Duration),                       /* tp_basicsize */
-    0,                                      /* tp_itemsize */
-    0,                                      /* tp_dealloc */
-    0,                                      /* tp_print */
-    0,                                      /* tp_getattr */
-    0,                                      /* tp_setattr */
-    0,                                      /* tp_as_async */
-    (reprfunc)Duration_repr,                /* tp_repr */
-    0,                                      /* tp_as_number */
-    0,                                      /* tp_as_sequence */
-    0,                                      /* tp_as_mapping */
-    0,                                      /* tp_hash  */
-    0,                                      /* tp_call */
-    (reprfunc)Duration_repr,                /* tp_str */
-    0,                                      /* tp_getattro */
-    0,                                      /* tp_setattro */
-    0,                                      /* tp_as_buffer */
-    Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */
-    "Duration",                             /* tp_doc */
-};
-
-#define new_duration(years, months, weeks, days, hours, minutes, seconds, microseconds) new_duration_ex(years, months, weeks, days, hours, minutes, seconds, microseconds, &Duration_type)
-
-typedef struct {
-    int is_date;
-    int is_time;
-    int is_datetime;
-    int is_duration;
-    int is_period;
-    int ambiguous;
-    int year;
-    int month;
-    int day;
-    int hour;
-    int minute;
-    int second;
-    int microsecond;
-    int offset;
-    int has_offset;
-    char *tzname;
-    int years;
-    int months;
-    int weeks;
-    int days;
-    int hours;
-    int minutes;
-    int seconds;
-    int microseconds;
-    int error;
-} Parsed;
-
-
-Parsed* new_parsed() {
-    Parsed *parsed;
-
-    if((parsed = malloc(sizeof *parsed)) != NULL) {
-        parsed->is_date = 0;
-        parsed->is_time = 0;
-        parsed->is_datetime = 0;
-        parsed->is_duration = 0;
-        parsed->is_period = 0;
-
-        parsed->ambiguous = 0;
-        parsed->year = 0;
-        parsed->month = 1;
-        parsed->day = 1;
-        parsed->hour = 0;
-        parsed->minute = 0;
-        parsed->second = 0;
-        parsed->microsecond = 0;
-        parsed->offset = 0;
-        parsed->has_offset = 0;
-        parsed->tzname = NULL;
-
-        parsed->years = 0;
-        parsed->months = 0;
-        parsed->weeks = 0;
-        parsed->days = 0;
-        parsed->hours = 0;
-        parsed->minutes = 0;
-        parsed->seconds = 0;
-        parsed->microseconds = 0;
-
-        parsed->error = -1;
-    }
-
-    return parsed;
-}
-
-
-/* -------------------------- Functions --------------------------*/
-
-Parsed* _parse_iso8601_datetime(char *str, Parsed *parsed) {
-    char* c;
-    int monthday = 0;
-    int week = 0;
-    int weekday = 1;
-    int ordinal;
-    int tz_sign = 0;
-    int leap = 0;
-    int separators = 0;
-    int time = 0;
-    int i;
-    int j;
-
-    // Assuming date only for now
-    parsed->is_date = 1;
-
-    c = str;
-
-    for (i = 0; i < 4; i++) {
-        if (*c >= '0' && *c <= '9') {
-            parsed->year = 10 * parsed->year + *c++ - '0';
-        } else {
-            parsed->error = PARSER_INVALID_ISO8601;
-
-            return NULL;
-        }
-    }
-
-    leap = is_leap(parsed->year);
-
-    // Optional separator
-    if (*c == '-') {
-        separators++;
-        c++;
-    }
-
-    // Checking for week dates
-    if (*c == 'W') {
-        c++;
-
-        i = 0;
-        while (*c != '\0' && *c != ' ' && *c != 'T') {
-            if (*c == '-') {
-                separators++;
-                c++;
-                continue;
-            }
-
-            week = 10 * week + *c++ - '0';
-
-            i++;
-        }
-
-        switch (i) {
-            case 2:
-                // Only week number
-                break;
-            case 3:
-                // Week with weekday
-                if (!(separators == 0 || separators == 2)) {
-                    // We should have 2 or no separator
-                    parsed->error = PARSER_INVALID_WEEK_DATE;
-
-                    return NULL;
-                }
-
-                weekday = week % 10;
-                week /= 10;
-
-                break;
-            default:
-                // Any other case is wrong
-                parsed->error = PARSER_INVALID_WEEK_DATE;
-
-                return NULL;
-        }
-
-        // Checks
-        if (week > 53 || (week > 52 && !is_long_year(parsed->year))) {
-            parsed->error = PARSER_INVALID_WEEK_NUMBER;
-
-            return NULL;
-        }
-
-        if (weekday > 7) {
-            parsed->error = PARSER_INVALID_WEEKDAY_NUMBER;
-
-            return NULL;
-        }
-
-        // Calculating ordinal day
-        ordinal = week * 7 + weekday - (week_day(parsed->year, 1, 4) + 3);
-
-        if (ordinal < 1) {
-            // Previous year
-            ordinal += days_in_year(parsed->year - 1);
-            parsed->year -= 1;
-            leap = is_leap(parsed->year);
-        }
-
-        if (ordinal > days_in_year(parsed->year)) {
-            // Next year
-            ordinal -= days_in_year(parsed->year);
-            parsed->year += 1;
-            leap = is_leap(parsed->year);
-        }
-
-        for (j = 1; j < 14; j++) {
-            if (ordinal <= MONTHS_OFFSETS[leap][j]) {
-                parsed->day = ordinal - MONTHS_OFFSETS[leap][j - 1];
-                parsed->month = j - 1;
-
-                break;
-            }
-        }
-    } else {
-        // At this point we need to check the number
-        // of characters until the end of the date part
-        // (or the end of the string).
-        //
-        // If two, we have only a month if there is a separator, it may be a time otherwise.
-        // If three, we have an ordinal date.
-        // If four, we have a complete date
-        i = 0;
-        while (*c != '\0' && *c != ' ' && *c != 'T') {
-            if (*c == '-') {
-                separators++;
-                c++;
-                continue;
-            }
-
-            if (!(*c >= '0' && *c <='9')) {
-                parsed->error = PARSER_INVALID_DATE;
-
-                return NULL;
-            }
-
-            monthday = 10 * monthday + *c++ - '0';
-
-            i++;
-        }
-
-        switch (i) {
-            case 0:
-                // No month/day specified (only a year)
-                break;
-            case 2:
-                if (!separators) {
-                    // The date looks like 201207
-                    // which is invalid for a date
-                    // But it might be a time in the form hhmmss
-                    parsed->ambiguous = 1;
-                } else if (separators > 1) {
-                    parsed->error = PARSER_INVALID_DATE;
-
-                    return NULL;
-                }
-
-                parsed->month = monthday;
-                break;
-            case 3:
-                // Ordinal day
-                if (separators > 1) {
-                    parsed->error = PARSER_INVALID_DATE;
-
-                    return NULL;
-                }
-
-                if (monthday < 1 || monthday > MONTHS_OFFSETS[leap][13]) {
-                    parsed->error = PARSER_INVALID_ORDINAL_DAY_FOR_YEAR;
-
-                    return NULL;
-                }
-
-                for (j = 1; j < 14; j++) {
-                    if (monthday <= MONTHS_OFFSETS[leap][j]) {
-                        parsed->day = monthday - MONTHS_OFFSETS[leap][j - 1];
-                        parsed->month = j - 1;
-
-                        break;
-                    }
-                }
-
-                break;
-            case 4:
-                // Month and day
-                parsed->month = monthday / 100;
-                parsed->day = monthday % 100;
-
-                break;
-            default:
-                parsed->error = PARSER_INVALID_MONTH_OR_DAY;
-
-                return NULL;
-        }
-    }
-
-    // Checks
-    if (separators && !monthday && !week) {
-        parsed->error = PARSER_INVALID_DATE;
-
-        return NULL;
-    }
-
-    if (parsed->month > 12) {
-        parsed->error = PARSER_INVALID_MONTH;
-
-        return NULL;
-    }
-
-    if (parsed->day > DAYS_PER_MONTHS[leap][parsed->month]) {
-        parsed->error = PARSER_INVALID_DAY_FOR_MONTH;
-
-        return NULL;
-    }
-
-    separators = 0;
-    if (*c == 'T' || *c == ' ') {
-        if (parsed->ambiguous) {
-            parsed->error = PARSER_INVALID_DATE;
-
-            return NULL;
-        }
-
-        // We have time so we have a datetime
-        parsed->is_datetime = 1;
-        parsed->is_date = 0;
-
-        c++;
-
-        // Grabbing time information
-        i = 0;
-        while (*c != '\0' && *c != '.' && *c != ',' && *c != 'Z' && *c != '+' && *c != '-') {
-            if (*c == ':') {
-                separators++;
-                c++;
-                continue;
-            }
-
-            if (!(*c >= '0' && *c <='9')) {
-                parsed->error = PARSER_INVALID_TIME;
-
-                return NULL;
-            }
-
-            time = 10 * time + *c++ - '0';
-            i++;
-        }
-
-        switch (i) {
-            case 2:
-                // Hours only
-                if (separators > 0) {
-                    // Extraneous separators
-                    parsed->error = PARSER_INVALID_TIME;
-
-                    return NULL;
-                }
-
-                parsed->hour = time;
-                break;
-            case 4:
-                // Hours and minutes
-                if (separators > 1) {
-                    // Extraneous separators
-                    parsed->error = PARSER_INVALID_TIME;
-
-                    return NULL;
-                }
-
-                parsed->hour = time / 100;
-                parsed->minute = time % 100;
-                break;
-            case 6:
-                // Hours, minutes and seconds
-                if (!(separators == 0 || separators == 2)) {
-                    // We should have either two separators or none
-                    parsed->error = PARSER_INVALID_TIME;
-
-                    return NULL;
-                }
-
-                parsed->hour = time / 10000;
-                parsed->minute = time / 100 % 100;
-                parsed->second = time % 100;
-                break;
-            default:
-                // Any other case is wrong
-                parsed->error = PARSER_INVALID_TIME;
-
-                return NULL;
-        }
-
-        // Checks
-        if (parsed->hour > 23) {
-            parsed->error = PARSER_INVALID_HOUR;
-
-            return NULL;
-        }
-
-        if (parsed->minute > 59) {
-            parsed->error = PARSER_INVALID_MINUTE;
-
-            return NULL;
-        }
-
-        if (parsed->second > 59) {
-            parsed->error = PARSER_INVALID_SECOND;
-
-            return NULL;
-        }
-
-        // Subsecond
-        if (*c == '.' || *c == ',') {
-            c++;
-
-            time = 0;
-            i = 0;
-            while (*c != '\0' && *c != 'Z' && *c != '+' && *c != '-') {
-                if (!(*c >= '0' && *c <='9')) {
-                    parsed->error = PARSER_INVALID_SUBSECOND;
-
-                    return NULL;
-                }
-
-                time = 10 * time + *c++ - '0';
-                i++;
-            }
-
-            // adjust to microseconds
-            if (i > 6) {
-                parsed->microsecond = time / pow(10, i - 6);
-            } else if (i <= 6) {
-                parsed->microsecond = time * pow(10, 6 - i);
-            }
-        }
-
-        // Timezone
-        if (*c == 'Z') {
-            parsed->has_offset = 1;
-            parsed->tzname = "UTC";
-            c++;
-        } else if (*c == '+' || *c == '-') {
-            tz_sign = 1;
-            if (*c == '-') {
-                tz_sign = -1;
-            }
-
-            parsed->has_offset = 1;
-            c++;
-
-            i = 0;
-            time = 0;
-            separators = 0;
-            while (*c != '\0') {
-                if (*c == ':') {
-                    separators++;
-                    c++;
-                    continue;
-                }
-
-                if (!(*c >= '0' && *c <= '9')) {
-                    parsed->error = PARSER_INVALID_TZ_OFFSET;
-
-                    return NULL;
-                }
-
-                time = 10 * time + *c++ - '0';
-                i++;
-            }
-
-            switch (i) {
-                case 2:
-                    // hh Format
-                    if (separators) {
-                        // Extraneous separators
-                        parsed->error = PARSER_INVALID_TZ_OFFSET;
-
-                        return NULL;
-                    }
-
-                    parsed->offset = tz_sign * (time * 3600);
-                    break;
-                case 4:
-                    // hhmm Format
-                    if (separators > 1) {
-                        // Extraneous separators
-                        parsed->error = PARSER_INVALID_TZ_OFFSET;
-
-                        return NULL;
-                    }
-
-                    parsed->offset = tz_sign * ((time / 100 * 3600) + (time % 100 * 60));
-                    break;
-                default:
-                    // Wrong format
-                    parsed->error = PARSER_INVALID_TZ_OFFSET;
-
-                    return NULL;
-            }
-        }
-    }
-
-    // At this point we should be at the end of the string
-    // If not, the string is invalid
-    if (*c != '\0') {
-        parsed->error = PARSER_INVALID_ISO8601;
-
-        return NULL;
-    }
-
-    return parsed;
-}
-
-
-Parsed* _parse_iso8601_duration(char *str, Parsed *parsed) {
-    char* c;
-    int value = 0;
-    int grabbed = 0;
-    int in_time = 0;
-    int in_fraction = 0;
-    int fraction_length = 0;
-    int has_fractional = 0;
-    int fraction = 0;
-    int has_ymd = 0;
-    int has_week = 0;
-    int has_month = 0;
-    int has_day = 0;
-    int has_hour = 0;
-    int has_minute = 0;
-    int has_second = 0;
-
-    c = str;
-
-    // Removing P operator
-    c++;
-
-    parsed->is_duration = 1;
-
-    for (; *c != '\0'; c++) {
-        switch (*c) {
-            case 'Y':
-                if (!grabbed || in_time || has_week || has_ymd) {
-                    // No value grabbed
-                    parsed->error = PARSER_INVALID_DURATION;
-
-                    return NULL;
-                }
-
-                if (fraction) {
-                    parsed->error = PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED;
-
-                    return NULL;
-                }
-
-                parsed->years = value;
-
-                grabbed = 0;
-                value = 0;
-                fraction = 0;
-                in_fraction = 0;
-                has_ymd = 1;
-
-                break;
-            case 'M':
-                if (!grabbed || has_week) {
-                    // No value grabbed
-                    parsed->error = PARSER_INVALID_DURATION;
-
-                    return NULL;
-                }
-
-                if (in_time) {
-                    if (has_second) {
-                        parsed->error = PARSER_INVALID_DURATION;
-
-                        return NULL;
-                    }
-
-                    if (has_fractional) {
-                        parsed->error = PARSER_INVALID_DURATION;
-
-                        return NULL;
-                    }
-
-                    parsed->minutes = value;
-                    if (fraction) {
-                        parsed->seconds = fraction * 6;
-                        has_fractional = 1;
-                    }
-
-                    has_minute = 1;
-                } else {
-                    if (fraction) {
-                        parsed->error = PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED;
-
-                        return NULL;
-                    }
-
-                    if (has_month || has_day) {
-                        parsed->error = PARSER_INVALID_DURATION;
-
-                        return NULL;
-                    }
-
-                    parsed->months = value;
-                    has_ymd = 1;
-                    has_month = 1;
-                }
-
-                grabbed = 0;
-                value = 0;
-                fraction = 0;
-                in_fraction = 0;
-
-                break;
-            case 'D':
-                if (!grabbed || in_time || has_week) {
-                    // No value grabbed
-                    parsed->error = PARSER_INVALID_DURATION;
-
-                    return NULL;
-                }
-
-                if (has_day) {
-                    parsed->error = PARSER_INVALID_DURATION;
-
-                    return NULL;
-                }
-
-                parsed->days = value;
-                if (fraction) {
-                    parsed->hours = fraction * 2.4;
-                    has_fractional = 1;
-                }
-
-                grabbed = 0;
-                value = 0;
-                fraction = 0;
-                in_fraction = 0;
-                has_ymd = 1;
-                has_day = 1;
-
-                break;
-            case 'T':
-                if (grabbed) {
-                    parsed->error = PARSER_INVALID_DURATION;
-
-                    return NULL;
-                }
-
-                in_time = 1;
-
-                break;
-            case 'H':
-                if (!grabbed || !in_time || has_week) {
-                    // No value grabbed
-                    parsed->error = PARSER_INVALID_DURATION;
-
-                    return NULL;
-                }
-
-                if (has_hour || has_second || has_minute) {
-                    parsed->error = PARSER_INVALID_DURATION;
-
-                    return NULL;
-                }
-
-                if (has_fractional) {
-                    parsed->error = PARSER_INVALID_DURATION;
-
-                    return NULL;
-                }
-
-                parsed->hours = value;
-                if (fraction) {
-                    parsed->minutes = fraction * 6;
-                    has_fractional = 1;
-                }
-
-                grabbed = 0;
-                value = 0;
-                fraction = 0;
-                in_fraction = 0;
-                has_hour = 1;
-
-                break;
-            case 'S':
-                if (!grabbed || !in_time || has_week) {
-                    // No value grabbed
-                    parsed->error = PARSER_INVALID_DURATION;
-
-                    return NULL;
-                }
-
-                if (has_second) {
-                    parsed->error = PARSER_INVALID_DURATION;
-
-                    return NULL;
-                }
-
-                if (has_fractional) {
-                    parsed->error = PARSER_INVALID_DURATION;
-
-                    return NULL;
-                }
-
-                if (fraction) {
-                    parsed->seconds = value;
-                    if (fraction_length > 6) {
-                        parsed->microseconds = fraction / pow(10, fraction_length - 6);
-                    } else {
-                        parsed->microseconds = fraction * pow(10, 6 - fraction_length);
-                    }
-                    has_fractional = 1;
-                } else {
-                    parsed->seconds = value;
-                }
-
-                grabbed = 0;
-                value = 0;
-                fraction = 0;
-                in_fraction = 0;
-                has_second = 1;
-
-                break;
-            case 'W':
-                if (!grabbed || in_time || has_ymd) {
-                    // No value grabbed
-                    parsed->error = PARSER_INVALID_DURATION;
-
-                    return NULL;
-                }
-
-                parsed->weeks = value;
-                if (fraction) {
-                    float days;
-                    days = fraction * 0.7;
-                    parsed->hours = (int) ((days - (int) days) * 24);
-                    parsed->days = (int) days;
-                }
-
-                grabbed = 0;
-                value = 0;
-                fraction = 0;
-                in_fraction = 0;
-                has_week = 1;
-
-                break;
-            case '.':
-                if (!grabbed || has_fractional) {
-                    // No value grabbed
-                    parsed->error = PARSER_INVALID_DURATION;
-
-                    return NULL;
-                }
-
-                in_fraction = 1;
-
-                break;
-            case ',':
-                if (!grabbed || has_fractional) {
-                    // No value grabbed
-                    parsed->error = PARSER_INVALID_DURATION;
-
-                    return NULL;
-                }
-
-                in_fraction = 1;
-
-                break;
-            default:
-                if (*c >= '0' && *c <='9') {
-                    if (in_fraction) {
-                        fraction = 10 * fraction + *c - '0';
-                        fraction_length++;
-                    } else {
-                        value = 10 * value + *c - '0';
-                        grabbed = 1;
-                    }
-                    break;
-                }
-
-                parsed->error = PARSER_INVALID_DURATION;
-
-                return NULL;
-        }
-    }
-
-    return parsed;
-}
-
-
-PyObject* parse_iso8601(PyObject *self, PyObject *args) {
-    char* str;
-    PyObject *obj;
-    PyObject *tzinfo;
-    Parsed *parsed = new_parsed();
-
-    if (!PyArg_ParseTuple(args, "s", &str)) {
-        PyErr_SetString(
-            PyExc_ValueError, "Invalid parameters"
-        );
-        free(parsed);
-        return NULL;
-    }
-
-    if (*str == 'P') {
-        // Duration (or interval)
-        if (_parse_iso8601_duration(str, parsed) == NULL) {
-            PyErr_SetString(
-                PyExc_ValueError, PARSER_ERRORS[parsed->error]
-            );
-
-            free(parsed);
-            return NULL;
-        }
-    } else if (_parse_iso8601_datetime(str, parsed) == NULL) {
-        PyErr_SetString(
-            PyExc_ValueError, PARSER_ERRORS[parsed->error]
-        );
-
-        free(parsed);
-        return NULL;
-    }
-
-    if (parsed->is_date) {
-        // Date only
-        if (parsed->ambiguous) {
-            // We can "safely" assume that the ambiguous
-            // date was actually a time in the form hhmmss
-            parsed->hour = parsed->year / 100;
-            parsed->minute = parsed->year % 100;
-            parsed->second = parsed->month;
-
-            obj = PyDateTimeAPI->Time_FromTime(
-                parsed->hour, parsed->minute, parsed->second, parsed->microsecond,
-                Py_BuildValue(""),
-                PyDateTimeAPI->TimeType
-            );
-        } else {
-            obj = PyDateTimeAPI->Date_FromDate(
-                parsed->year, parsed->month, parsed->day,
-                PyDateTimeAPI->DateType
-            );
-        }
-    } else if (parsed->is_datetime) {
-        if (!parsed->has_offset) {
-            tzinfo = Py_BuildValue("");
-        } else {
-            tzinfo = new_fixed_offset(parsed->offset, parsed->tzname);
-        }
-
-        obj = PyDateTimeAPI->DateTime_FromDateAndTime(
-            parsed->year,
-            parsed->month,
-            parsed->day,
-            parsed->hour,
-            parsed->minute,
-            parsed->second,
-            parsed->microsecond,
-            tzinfo,
-            PyDateTimeAPI->DateTimeType
-        );
-
-        Py_DECREF(tzinfo);
-    } else if (parsed->is_duration) {
-        obj = new_duration(
-            parsed->years, parsed->months, parsed->weeks, parsed->days,
-            parsed->hours, parsed->minutes, parsed->seconds, parsed->microseconds
-        );
-    } else {
-        free(parsed);
-        return NULL;
-    }
-
-    free(parsed);
-
-    return obj;
-}
-
-
-/* ------------------------------------------------------------------------- */
-
-static PyMethodDef helpers_methods[] = {
-    {
-        "parse_iso8601",
-        (PyCFunction) parse_iso8601,
-        METH_VARARGS,
-        PyDoc_STR("Parses a ISO8601 string into a tuple.")
-    },
-    {NULL}
-};
-
-
-/* ------------------------------------------------------------------------- */
-
-static struct PyModuleDef moduledef = {
-    PyModuleDef_HEAD_INIT,
-    "_iso8601",
-    NULL,
-    -1,
-    helpers_methods,
-    NULL,
-    NULL,
-    NULL,
-    NULL,
-};
-
-PyMODINIT_FUNC
-PyInit__iso8601(void)
-{
-    PyObject *module;
-
-    PyDateTime_IMPORT;
-
-    module = PyModule_Create(&moduledef);
-
-    if (module == NULL)
-        return NULL;
-
-    // FixedOffset declaration
-    FixedOffset_type.tp_new = PyType_GenericNew;
-    FixedOffset_type.tp_base = PyDateTimeAPI->TZInfoType;
-    FixedOffset_type.tp_methods = FixedOffset_methods;
-    FixedOffset_type.tp_members = FixedOffset_members;
-    FixedOffset_type.tp_init = (initproc)FixedOffset_init;
-
-    if (PyType_Ready(&FixedOffset_type) < 0)
-        return NULL;
-
-    // Duration declaration
-    Duration_type.tp_new = PyType_GenericNew;
-    Duration_type.tp_members = Duration_members;
-    Duration_type.tp_init = (initproc)Duration_init;
-
-    if (PyType_Ready(&Duration_type) < 0)
-        return NULL;
-
-    Py_INCREF(&FixedOffset_type);
-    Py_INCREF(&Duration_type);
-
-    PyModule_AddObject(module, "TZFixedOffset", (PyObject *)&FixedOffset_type);
-    PyModule_AddObject(module, "Duration", (PyObject *)&Duration_type);
-
-    return module;
-}
diff --git a/pendulum/parsing/_iso8601.pyi b/pendulum/parsing/_iso8601.pyi
deleted file mode 100644
index 761fe6b..0000000
--- a/pendulum/parsing/_iso8601.pyi
+++ /dev/null
@@ -1,21 +0,0 @@
-from __future__ import annotations
-
-from datetime import date
-from datetime import datetime
-from datetime import time
-
-class Duration:
-    years: int = 0
-    months: int = 0
-    weeks: int = 0
-    days: int = 0
-    remaining_days: int = 0
-    hours: int = 0
-    minutes: int = 0
-    seconds: int = 0
-    remaining_seconds: int = 0
-    microseconds: int = 0
-
-def parse_iso8601(
-    text: str,
-) -> datetime | date | time | Duration: ...
diff --git a/pendulum/parsing/iso8601.py b/pendulum/parsing/iso8601.py
index 4f99175..cc4dd7a 100644
--- a/pendulum/parsing/iso8601.py
+++ b/pendulum/parsing/iso8601.py
@@ -185,7 +185,7 @@ def parse_iso8601(
         if ambiguous_date:
             # We can "safely" assume that the ambiguous date
             # was actually a time in the form hhmmss
-            hhmmss = f"{year!s}{str(month):0>2}"
+            hhmmss = f"{year!s}{month!s:0>2}"
 
             return datetime.time(int(hhmmss[:2]), int(hhmmss[2:4]), int(hhmmss[4:]))
 
@@ -255,7 +255,7 @@ def parse_iso8601(
             tzinfo = FixedTimezone(offset)
 
     if is_time:
-        return datetime.time(hour, minute, second, microsecond)
+        return datetime.time(hour, minute, second, microsecond, tzinfo=tzinfo)
 
     return datetime.datetime(
         year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo
diff --git a/poetry.lock b/poetry.lock
index 6f6e448..4addbfb 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -421,22 +421,6 @@ files = [
     {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
 ]
 
-[[package]]
-name = "meson"
-version = "0.63.2"
-description = "A high performance build system"
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "meson-0.63.2-py3-none-any.whl", hash = "sha256:64a83ef257b2962b52c8b07ad9ec536c2de1b72fd9f14bcd9c21fe45730edd46"},
-    {file = "meson-0.63.2.tar.gz", hash = "sha256:16222f17ef76be0542c91c07994f9676ae879f46fc21c0c786a21ef2cb518bbf"},
-]
-
-[package.extras]
-ninja = ["ninja (>=1.8.2)"]
-progress = ["tqdm"]
-typing = ["mypy", "typing-extensions"]
-
 [[package]]
 name = "mkdocs"
 version = "1.3.0"
@@ -521,32 +505,6 @@ files = [
     {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
 ]
 
-[[package]]
-name = "ninja"
-version = "1.10.2.3"
-description = "Ninja is a small build system with a focus on speed"
-optional = false
-python-versions = "*"
-files = [
-    {file = "ninja-1.10.2.3-py2.py3-none-macosx_10_9_universal2.macosx_10_9_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:d5e0275d28997a750a4f445c00bdd357b35cc334c13cdff13edf30e544704fbd"},
-    {file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ea785bf6a15727040835256577239fa3cf5da0d60e618c307aa5efc31a1f0ce"},
-    {file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29570a18d697fc84d361e7e6330f0021f34603ae0fcb0ef67ae781e9814aae8d"},
-    {file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21a1d84d4c7df5881bfd86c25cce4cf7af44ba2b8b255c57bc1c434ec30a2dfc"},
-    {file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9ca8dbece144366d5f575ffc657af03eb11c58251268405bc8519d11cf42f113"},
-    {file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:279836285975e3519392c93c26e75755e8a8a7fafec9f4ecbb0293119ee0f9c6"},
-    {file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cc8b31b5509a2129e4d12a35fc21238c157038022560aaf22e49ef0a77039086"},
-    {file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_i686.whl", hash = "sha256:688167841b088b6802e006f911d911ffa925e078c73e8ef2f88286107d3204f8"},
-    {file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:840a0b042d43a8552c4004966e18271ec726e5996578f28345d9ce78e225b67e"},
-    {file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_s390x.whl", hash = "sha256:84be6f9ec49f635dc40d4b871319a49fa49b8d55f1d9eae7cd50d8e57ddf7a85"},
-    {file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6bd76a025f26b9ae507cf8b2b01bb25bb0031df54ed685d85fc559c411c86cf4"},
-    {file = "ninja-1.10.2.3-py2.py3-none-win32.whl", hash = "sha256:740d61fefb4ca13573704ee8fe89b973d40b8dc2a51aaa4e9e68367233743bb6"},
-    {file = "ninja-1.10.2.3-py2.py3-none-win_amd64.whl", hash = "sha256:0560eea57199e41e86ac2c1af0108b63ae77c3ca4d05a9425a750e908135935a"},
-    {file = "ninja-1.10.2.3.tar.gz", hash = "sha256:e1b86ad50d4e681a7dbdff05fc23bb52cb773edb90bc428efba33fa027738408"},
-]
-
-[package.extras]
-test = ["codecov (>=2.0.5)", "coverage (>=4.2)", "flake8 (>=3.0.4)", "pytest (>=4.5.0)", "pytest-cov (>=2.7.1)", "pytest-runner (>=5.1)", "pytest-virtualenv (>=1.7.0)", "virtualenv (>=15.0.3)"]
-
 [[package]]
 name = "nodeenv"
 version = "1.7.0"
@@ -655,7 +613,7 @@ name = "pycparser"
 version = "2.21"
 description = "C parser in Python"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = "*"
 files = [
     {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
     {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
@@ -663,15 +621,18 @@ files = [
 
 [[package]]
 name = "pygments"
-version = "2.12.0"
+version = "2.16.1"
 description = "Pygments is a syntax highlighting package written in Python."
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
-    {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"},
-    {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"},
+    {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"},
+    {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"},
 ]
 
+[package.extras]
+plugins = ["importlib-metadata"]
+
 [[package]]
 name = "pymdown-extensions"
 version = "10.0.1"
@@ -1234,4 +1195,4 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.7"
-content-hash = "7a4cee3a29551de99a31bcf181c7a90cdf9c2979fb3cfbb3a3981d6a0b1c7488"
+content-hash = "658e22a0828244e01d1639cbacfbaa84a6ad345aade89e5e28722fb39401db37"
diff --git a/pyproject.toml b/pyproject.toml
index 91bd700..f5c5e71 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,13 +15,14 @@ packages = [
     { include = "tests", format = "sdist" },
 ]
 include = [
-    { path = "meson.build", format = "sdist" },
     { path = "pendulum/py.typed" },
-    # C extensions must be included in the wheel distributions
-    { path = "pendulum/_extensions/*.so", format = "wheel" },
-    { path = "pendulum/_extensions/*.pyd", format = "wheel" },
-    { path = "pendulum/parsing/*.so", format = "wheel" },
-    { path = "pendulum/parsing/*.pyd", format = "wheel" },
+    # Typing stubs
+    { path = "*.pyi"},
+    # Rust source
+    { path = "rust", format = "sdist" },
+    # Rust extension
+    { path = "_pendulum*.so", format = "wheel" },
+    { path = "_pendulum*.pyd", format = "wheel" },
 ]
 
 
@@ -58,13 +59,6 @@ babel = "^2.10.3"
 cleo = ">=1,<3"
 tox = "^3.25.1"
 
-[tool.poetry.group.build]
-optional = true
-
-[tool.poetry.group.build.dependencies]
-meson = "^0.63.2"
-ninja = "^1.10.2.3"
-
 [tool.poetry.group.benchmark.dependencies]
 pytest-codspeed = "^1.2.2"
 
@@ -127,6 +121,7 @@ required-imports = ["from __future__ import annotations"]
 
 [tool.ruff.extend-per-file-ignores]
 "build.py" = ["I002"]
+"clock" = ["RUF012"]
 
 [tool.mypy]
 strict = true
@@ -219,5 +214,5 @@ omit = [
 ]
 
 [build-system]
-requires = ["poetry-core>=1.1.0a6", "meson", "ninja"]
+requires = ["poetry-core>=1.6.1", "maturin>=1,<2"]
 build-backend = "poetry.core.masonry.api"
diff --git a/rust/constants.rs b/rust/constants.rs
new file mode 100644
index 0000000..3fea9c0
--- /dev/null
+++ b/rust/constants.rs
@@ -0,0 +1,56 @@
+pub const EPOCH_YEAR: u32 = 1970;
+
+pub const DAYS_PER_N_YEAR: u32 = 365;
+pub const DAYS_PER_L_YEAR: u32 = 366;
+
+pub const SECS_PER_MIN: u32 = 60;
+pub const SECS_PER_HOUR: u32 = SECS_PER_MIN * 60;
+pub const SECS_PER_DAY: u32 = SECS_PER_HOUR * 24;
+
+// 400-year chunks always have 146097 days (20871 weeks).
+pub const DAYS_PER_400_YEARS: u32 = 146_097;
+pub const SECS_PER_400_YEARS: u64 = DAYS_PER_400_YEARS as u64 * SECS_PER_DAY as u64;
+
+// 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.
+pub const SECS_PER_100_YEARS: [u64; 2] = [
+    (76 * DAYS_PER_N_YEAR as u64 + 24 * DAYS_PER_L_YEAR as u64) * SECS_PER_DAY as u64,
+    (75 * DAYS_PER_N_YEAR as u64 + 25 * DAYS_PER_L_YEAR as u64) * SECS_PER_DAY as u64,
+];
+
+// 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.
+#[allow(clippy::erasing_op)]
+pub const SECS_PER_4_YEARS: [u32; 2] = [
+    (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY,
+    (3 * DAYS_PER_N_YEAR + DAYS_PER_L_YEAR) * SECS_PER_DAY,
+];
+
+// The number of seconds in non-leap and leap years respectively.
+pub const SECS_PER_YEAR: [u32; 2] = [
+    DAYS_PER_N_YEAR * SECS_PER_DAY,
+    DAYS_PER_L_YEAR * SECS_PER_DAY,
+];
+
+// The month lengths in non-leap and leap years respectively.
+pub const DAYS_PER_MONTHS: [[i32; 13]; 2] = [
+    [-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.
+pub const MONTHS_OFFSETS: [[i32; 14]; 2] = [
+    [
+        -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,
+    ],
+];
+
+pub const DAY_OF_WEEK_TABLE: [u32; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
+
+pub const TM_JANUARY: usize = 0;
+pub const TM_DECEMBER: usize = 11;
diff --git a/rust/helpers.rs b/rust/helpers.rs
new file mode 100644
index 0000000..364075a
--- /dev/null
+++ b/rust/helpers.rs
@@ -0,0 +1,122 @@
+use crate::constants::{
+    DAYS_PER_L_YEAR, DAYS_PER_N_YEAR, DAY_OF_WEEK_TABLE, EPOCH_YEAR, MONTHS_OFFSETS,
+    SECS_PER_100_YEARS, SECS_PER_400_YEARS, SECS_PER_4_YEARS, SECS_PER_DAY, SECS_PER_HOUR,
+    SECS_PER_MIN, SECS_PER_YEAR, TM_DECEMBER, TM_JANUARY,
+};
+
+fn p(year: i32) -> i32 {
+    year + year / 4 - year / 100 + year / 400
+}
+
+pub fn is_leap(year: i32) -> bool {
+    year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
+}
+
+pub fn is_long_year(year: i32) -> bool {
+    (p(year) % 7 == 4) || (p(year - 1) % 7 == 3)
+}
+
+pub fn days_in_year(year: i32) -> u32 {
+    if is_leap(year) {
+        return DAYS_PER_L_YEAR;
+    }
+
+    DAYS_PER_N_YEAR
+}
+
+pub fn week_day(year: i32, month: u32, day: u32) -> u32 {
+    let y: i32 = year - i32::from(month < 3);
+
+    let w: i32 = (p(y) + DAY_OF_WEEK_TABLE[(month - 1) as usize] as i32 + day as i32) % 7;
+
+    if w == 0 {
+        return 7;
+    }
+
+    w.unsigned_abs()
+}
+
+pub fn day_number(year: i32, month: u8, day: u8) -> i32 {
+    let m = i32::from((month + 9) % 12);
+    let y = year - m / 10;
+
+    365 * y + y / 4 - y / 100 + y / 400 + (m * 306 + 5) / 10 + (i32::from(day) - 1)
+}
+
+pub fn local_time(
+    unix_time: f64,
+    utc_offset: isize,
+    microsecond: usize,
+) -> (usize, usize, usize, usize, usize, usize, usize) {
+    let mut year: usize = EPOCH_YEAR as usize;
+    let mut seconds: isize = unix_time.floor() as isize;
+
+    // Shift to a base year that is 400-year aligned.
+    if seconds >= 0 {
+        seconds -= (10957 * SECS_PER_DAY as usize) as isize;
+        year += 30; // == 2000
+    } else {
+        seconds += ((146_097 - 10957) * SECS_PER_DAY as usize) as isize;
+        year -= 370; // == 1600
+    }
+
+    seconds += utc_offset;
+
+    // Handle years in chunks of 400/100/4/1
+    year += 400 * (seconds / SECS_PER_400_YEARS as isize) as usize;
+    seconds %= SECS_PER_400_YEARS as isize;
+    if seconds < 0 {
+        seconds += SECS_PER_400_YEARS as isize;
+        year -= 400;
+    }
+
+    let mut leap_year = 1; // 4-century aligned
+    let mut sec_per_100years = SECS_PER_100_YEARS[leap_year] as isize;
+
+    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] as isize;
+    }
+
+    let mut sec_per_4years = SECS_PER_4_YEARS[leap_year] as isize;
+    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] as isize;
+    }
+
+    let mut sec_per_year = SECS_PER_YEAR[leap_year] as isize;
+    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] as isize;
+    }
+
+    // Handle months and days
+    let mut month = TM_DECEMBER + 1;
+    let mut day: usize = (seconds / (SECS_PER_DAY as isize) + 1) as usize;
+    seconds %= SECS_PER_DAY as isize;
+
+    let mut month_offset: usize;
+    while month != (TM_JANUARY + 1) {
+        month_offset = MONTHS_OFFSETS[leap_year][month] as usize;
+        if day > month_offset {
+            day -= month_offset;
+            break;
+        }
+
+        month -= 1;
+    }
+
+    // Handle hours, minutes and seconds
+    let hour: usize = (seconds / SECS_PER_HOUR as isize) as usize;
+    seconds %= SECS_PER_HOUR as isize;
+    let minute: usize = (seconds / SECS_PER_MIN as isize) as usize;
+    let second: usize = (seconds % SECS_PER_MIN as isize) as usize;
+
+    (year, month, day, hour, minute, second, microsecond)
+}
diff --git a/rust/lib.rs b/rust/lib.rs
new file mode 100644
index 0000000..bd0f1f6
--- /dev/null
+++ b/rust/lib.rs
@@ -0,0 +1,12 @@
+extern crate core;
+
+#[cfg(feature = "mimalloc")]
+#[global_allocator]
+static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
+
+mod constants;
+mod helpers;
+mod parsing;
+mod python;
+
+pub use python::_pendulum;
diff --git a/rust/parsing.rs b/rust/parsing.rs
new file mode 100644
index 0000000..757a3e3
--- /dev/null
+++ b/rust/parsing.rs
@@ -0,0 +1,905 @@
+use core::str;
+use std::{fmt, str::CharIndices};
+
+use crate::{
+    constants::MONTHS_OFFSETS,
+    helpers::{days_in_year, is_leap, is_long_year, week_day},
+};
+
+#[derive(Debug, Clone)]
+pub struct ParseError {
+    index: usize,
+    message: String,
+}
+
+impl fmt::Display for ParseError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} (Position: {})", self.message, self.index)
+    }
+}
+
+pub struct ParsedDateTime {
+    pub year: u32,
+    pub month: u32,
+    pub day: u32,
+    pub hour: u32,
+    pub minute: u32,
+    pub second: u32,
+    pub microsecond: u32,
+    pub offset: Option<i32>,
+    pub has_offset: bool,
+    pub tzname: Option<String>,
+    pub has_date: bool,
+    pub has_time: bool,
+    pub extended_date_format: bool,
+    pub time_is_midnight: bool,
+}
+
+impl ParsedDateTime {
+    pub fn new() -> ParsedDateTime {
+        ParsedDateTime {
+            year: 0,
+            month: 1,
+            day: 1,
+            hour: 0,
+            minute: 0,
+            second: 0,
+            microsecond: 0,
+            offset: None,
+            has_offset: false,
+            tzname: None,
+            has_date: false,
+            has_time: false,
+            extended_date_format: false,
+            time_is_midnight: false,
+        }
+    }
+}
+
+pub struct ParsedDuration {
+    pub years: u32,
+    pub months: u32,
+    pub weeks: u32,
+    pub days: u32,
+    pub hours: u32,
+    pub minutes: u32,
+    pub seconds: u32,
+    pub microseconds: u32,
+}
+
+impl ParsedDuration {
+    pub fn new() -> ParsedDuration {
+        ParsedDuration {
+            years: 0,
+            months: 0,
+            weeks: 0,
+            days: 0,
+            hours: 0,
+            minutes: 0,
+            seconds: 0,
+            microseconds: 0,
+        }
+    }
+}
+
+pub struct Parsed {
+    pub datetime: Option<ParsedDateTime>,
+    pub duration: Option<ParsedDuration>,
+    pub second_datetime: Option<ParsedDateTime>,
+}
+
+impl Parsed {
+    pub fn new() -> Parsed {
+        Parsed {
+            datetime: None,
+            duration: None,
+            second_datetime: None,
+        }
+    }
+}
+
+pub struct Parser<'a> {
+    /// Input to parse.
+    src: &'a str,
+    /// Iterator used for getting characters from `src`.
+    chars: CharIndices<'a>,
+    /// Current byte offset into `src`.
+    idx: usize,
+    /// Current character
+    current: char,
+}
+
+impl<'a> Parser<'a> {
+    /// Creates a new parser from a &str.
+    pub fn new(input: &'a str) -> Parser<'a> {
+        let mut p = Parser {
+            src: input,
+            chars: input.char_indices(),
+            idx: 0,
+            current: '\0',
+        };
+        p.inc();
+        p
+    }
+
+    /// Increments the parser if the end of the input has not been reached.
+    /// Returns whether or not it was able to advance.
+    fn inc(&mut self) -> Option<char> {
+        if let Some((i, ch)) = self.chars.next() {
+            self.idx = i;
+            self.current = ch;
+            Some(ch)
+        } else {
+            self.idx = self.src.len();
+            self.current = '\0';
+            None
+        }
+    }
+
+    fn parse_error(&mut self, message: String) -> ParseError {
+        ParseError {
+            index: self.idx,
+            message,
+        }
+    }
+
+    fn unexpected_character_error(
+        &mut self,
+        field_name: &str,
+        expected_character_count: usize,
+    ) -> ParseError {
+        if self.end() {
+            return self.parse_error(format!(
+                "Unexpected end of string while parsing {}. Expected {} more character{}.",
+                field_name,
+                expected_character_count,
+                if expected_character_count == 1 {
+                    ""
+                } else {
+                    "s"
+                }
+            ));
+        }
+
+        self.parse_error(format!(
+            "Invalid character while parsing {}: {}.",
+            field_name, self.current,
+        ))
+    }
+
+    /// Returns true if the parser has reached the end of the input.
+    fn end(&self) -> bool {
+        self.idx >= self.src.len()
+    }
+
+    fn parse_integer(&mut self, length: usize, field_name: &str) -> Result<u32, ParseError> {
+        let mut value: u32 = 0;
+
+        for i in 0..length {
+            if self.end() {
+                return Err(self.parse_error(format!(
+                    "Unexpected end of string while parsing \"{}\". Expected {} more character{}",
+                    field_name,
+                    length - i,
+                    if (length - i) != 1 { "s" } else { "" }
+                )));
+            }
+
+            if let Some(digit) = self.current.to_digit(10) {
+                value = 10 * value + digit;
+                self.inc();
+            } else {
+                return Err(self.unexpected_character_error(field_name, length - i));
+            }
+        }
+
+        Ok(value)
+    }
+
+    pub fn parse(&mut self) -> Result<Parsed, ParseError> {
+        let mut parsed = Parsed::new();
+
+        if self.current == 'P' {
+            // Duration (and possibly time interval)
+            self.parse_duration(&mut parsed)?;
+        } else {
+            self.parse_datetime(&mut parsed)?;
+        }
+
+        Ok(parsed)
+    }
+
+    fn parse_datetime(&mut self, parsed: &mut Parsed) -> Result<(), ParseError> {
+        let mut datetime = ParsedDateTime::new();
+
+        if self.current == 'T' {
+            self.parse_time(&mut datetime, false)?;
+
+            if !self.end() {
+                return Err(self.parse_error("Unconverted data remains".to_string()));
+            }
+
+            match &parsed.datetime {
+                Some(_) => {
+                    parsed.second_datetime = Some(datetime);
+                }
+                None => match &parsed.duration {
+                    Some(_) => {
+                        parsed.second_datetime = Some(datetime);
+                    }
+                    None => {
+                        parsed.datetime = Some(datetime);
+                    }
+                },
+            }
+
+            return Ok(());
+        }
+
+        datetime.year = self.parse_integer(2, "year")?;
+
+        if self.current == ':' {
+            // Time in extended format
+            datetime.hour = datetime.year;
+            datetime.year = 0;
+            datetime.extended_date_format = true;
+            self.parse_time(&mut datetime, true)?;
+
+            if !self.end() {
+                return Err(self.parse_error("Unconverted data remains".to_string()));
+            }
+
+            match &parsed.datetime {
+                Some(_) => {
+                    parsed.second_datetime = Some(datetime);
+                }
+                None => match &parsed.duration {
+                    Some(_) => {
+                        parsed.second_datetime = Some(datetime);
+                    }
+                    None => {
+                        parsed.datetime = Some(datetime);
+                    }
+                },
+            }
+
+            return Ok(());
+        }
+
+        datetime.has_date = true;
+        datetime.year = datetime.year * 100 + self.parse_integer(2, "year")?;
+
+        if self.current == '-' {
+            self.inc();
+            datetime.extended_date_format = true;
+
+            if self.current == 'W' {
+                // ISO week and day in extended format (i.e. Www-D)
+                self.inc();
+
+                let iso_week = self.parse_integer(2, "iso week")?;
+                let mut iso_day: u32 = 1;
+
+                if !self.end() && self.current != ' ' && self.current != 'T' {
+                    // Optional day
+                    if self.current != '-' {
+                        return Err(self.parse_error(format!(
+                            "Invalid character \"{}\" while parsing {}",
+                            self.current, "date separator"
+                        )));
+                    }
+
+                    self.inc();
+
+                    iso_day = self.parse_integer(1, "iso day")?;
+                }
+
+                let (year, month, day) = self.iso_to_ymd(datetime.year, iso_week, iso_day)?;
+
+                datetime.year = year;
+                datetime.month = month;
+                datetime.day = day;
+            } else {
+                /*
+                Month and day in extended format (MM-DD) or ordinal date (DDD)
+                We'll assume first that the next part is a month and adjust if not.
+                */
+                datetime.month = self.parse_integer(2, "month")?;
+
+                if !self.end() && self.current != ' ' && self.current != 'T' {
+                    if self.current == '-' {
+                        // Optional day
+                        self.inc();
+                        datetime.day = self.parse_integer(2, "day")?;
+                    } else {
+                        // Ordinal day
+                        let ordinal_day =
+                            (datetime.month * 10 + self.parse_integer(1, "ordinal day")?) as i32;
+
+                        let (year, month, day) =
+                            self.ordinal_to_ymd(datetime.year, ordinal_day, false)?;
+
+                        datetime.year = year;
+                        datetime.month = month;
+                        datetime.day = day;
+                    }
+                } else {
+                    datetime.day = 1;
+                }
+            }
+        } else if self.current == 'W' {
+            // Compact ISO week and day (WwwD)
+            self.inc();
+
+            let iso_week = self.parse_integer(2, "iso week")?;
+            let mut iso_day: u32 = 1;
+
+            if !self.end() && self.current != ' ' && self.current != 'T' {
+                iso_day = self.parse_integer(1, "iso day")?;
+            }
+
+            match self.iso_to_ymd(datetime.year, iso_week, iso_day) {
+                Ok((year, month, day)) => {
+                    datetime.year = year;
+                    datetime.month = month;
+                    datetime.day = day;
+                }
+                Err(error) => return Err(error),
+            }
+        } else {
+            /*
+            Month and day in compact format (MMDD) or ordinal date (DDD)
+            We'll assume first that the next part is a month and adjust if not.
+            */
+            datetime.month = self.parse_integer(2, "month")?;
+            let mut ordinal_day = self.parse_integer(1, "ordinal day")? as i32;
+
+            if self.end() || self.current == ' ' || self.current == 'T' {
+                // Ordinal day
+                ordinal_day += datetime.month as i32 * 10;
+
+                let (year, month, day) = self.ordinal_to_ymd(datetime.year, ordinal_day, false)?;
+
+                datetime.year = year;
+                datetime.month = month;
+                datetime.day = day;
+            } else {
+                // Day
+                datetime.day = ordinal_day as u32 * 10 + self.parse_integer(1, "day")?;
+            }
+        }
+
+        if !self.end() {
+            self.parse_time(&mut datetime, false)?;
+        }
+
+        if !self.end() {
+            if self.current == '/' && parsed.datetime.is_none() && parsed.duration.is_none() {
+                // Interval
+                parsed.datetime = Some(datetime);
+
+                self.inc();
+
+                if self.current == 'P' {
+                    // Duration
+                    self.parse_duration(parsed)?;
+                } else {
+                    self.parse_datetime(parsed)?;
+                }
+
+                return Ok(());
+            }
+
+            return Err(self.parse_error("Unconverted data remains".to_string()));
+        }
+
+        match &parsed.datetime {
+            Some(_) => {
+                parsed.second_datetime = Some(datetime);
+            }
+            None => match &parsed.duration {
+                Some(_) => {
+                    parsed.second_datetime = Some(datetime);
+                }
+                None => {
+                    parsed.datetime = Some(datetime);
+                }
+            },
+        }
+
+        Ok(())
+    }
+
+    fn parse_time(
+        &mut self,
+        datetime: &mut ParsedDateTime,
+        skip_hour: bool,
+    ) -> Result<(), ParseError> {
+        // TODO: Add support for decimal units
+
+        // Date/Time separator
+        if self.current != 'T' && self.current != ' ' && !skip_hour {
+            return Err(self.parse_error(format!(
+                "Invalid character \"{}\" while parsing {}",
+                self.current, "date and time separator (\"T\" or \" \")"
+            )));
+        }
+
+        datetime.has_time = true;
+
+        if !skip_hour {
+            self.inc();
+
+            // Hour
+            datetime.hour = self.parse_integer(2, "hour")?;
+        }
+
+        if !self.end() && self.current != 'Z' && self.current != '+' && self.current != '-' {
+            // Optional minute and second
+            if self.current == ':' {
+                // Minute and second in extended format (mm:ss)
+                self.inc();
+
+                // Minute
+                datetime.minute = self.parse_integer(2, "minute")?;
+
+                if !self.end() && self.current != 'Z' && self.current != '+' && self.current != '-'
+                {
+                    // Optional second
+                    if self.current != ':' {
+                        return Err(self.parse_error(format!(
+                            "Invalid character \"{}\" while parsing {}",
+                            self.current, "time separator (\":\")"
+                        )));
+                    }
+
+                    self.inc();
+
+                    // Second
+                    datetime.second = self.parse_integer(2, "second")?;
+
+                    if self.current == '.' || self.current == ',' {
+                        // Optional fractional second
+                        self.inc();
+
+                        datetime.microsecond = 0;
+                        let mut i: u8 = 0;
+
+                        while i < 6 {
+                            if let Some(digit) = self.current.to_digit(10) {
+                                datetime.microsecond = datetime.microsecond * 10 + digit;
+                            } else if i == 0 {
+                                // One digit minimum is required
+                                return Err(self.unexpected_character_error("subsecond", 1));
+                            } else {
+                                break;
+                            }
+
+                            self.inc();
+                            i += 1;
+                        }
+
+                        // Drop extraneous digits
+                        while self.current.is_ascii_digit() {
+                            self.inc();
+                        }
+
+                        // Expand missing microsecond
+                        while i < 6 {
+                            datetime.microsecond *= 10;
+                            i += 1;
+                        }
+                    }
+
+                    if !datetime.extended_date_format {
+                        return Err(self.parse_error("Cannot combine \"basic\" date format with \"extended\" time format (Should be either `YYYY-MM-DDThh:mm:ss` or `YYYYMMDDThhmmss`).".to_string()));
+                    }
+                }
+            } else {
+                // Minute and second in compact format (mmss)
+
+                // Minute
+                datetime.minute = self.parse_integer(2, "minute")?;
+
+                if !self.end() && self.current != 'Z' && self.current != '+' && self.current != '-'
+                {
+                    // Optional second
+
+                    datetime.second = self.parse_integer(2, "second")?;
+
+                    if self.current == '.' || self.current == ',' {
+                        // Optional fractional second
+                        self.inc();
+
+                        datetime.microsecond = 0;
+                        let mut i: u8 = 0;
+
+                        while i < 6 {
+                            if let Some(digit) = self.current.to_digit(10) {
+                                datetime.microsecond = datetime.microsecond * 10 + digit;
+                            } else if i == 0 {
+                                // One digit minimum is required
+                                return Err(self.unexpected_character_error("subsecond", 1));
+                            } else {
+                                break;
+                            }
+
+                            self.inc();
+                            i += 1;
+                        }
+
+                        // Drop extraneous digits
+                        while self.current.is_ascii_digit() {
+                            self.inc();
+                        }
+
+                        // Expand missing microsecond
+                        while i < 6 {
+                            datetime.microsecond *= 10;
+                            i += 1;
+                        }
+                    }
+                }
+
+                if datetime.extended_date_format {
+                    return Err(self.parse_error("Cannot combine \"extended\" date format with \"basic\" time format (Should be either `YYYY-MM-DDThh:mm:ss` or `YYYYMMDDThhmmss`).".to_string()));
+                }
+            }
+        }
+
+        if datetime.hour == 24
+            && datetime.minute == 0
+            && datetime.second == 0
+            && datetime.microsecond == 0
+        {
+            // Special case for 24:00:00, which is valid for ISO 8601.
+            // This is equivalent to 00:00:00 the next day.
+            // We will store the information for now.
+            datetime.time_is_midnight = true;
+        }
+
+        if self.current == 'Z' {
+            // UTC
+            datetime.offset = Some(0);
+            datetime.tzname = Some("UTC".to_string());
+            self.inc();
+        } else if matches!(self.current, '+' | '-') {
+            // Optional timezone offset
+            let tzsign = if self.current == '+' { 1 } else { -1 };
+            self.inc();
+            // Offset hour
+            let tzhour = self.parse_integer(2, "timezone hour")? as i32;
+            if self.current == ':' {
+                // Optional separator
+                self.inc();
+            }
+            let mut tzminute = if self.end() {
+                0
+            } else {
+                // Optional minute
+                self.parse_integer(2, "timezone minute")? as i32
+            };
+            tzminute += tzhour * 60;
+            tzminute *= tzsign;
+            if tzminute > 24 * 60 {
+                return Err(self.parse_error("Timezone offset is too large".to_string()));
+            }
+            datetime.offset = Some(tzminute * 60);
+        }
+
+        Ok(())
+    }
+
+    fn parse_duration(&mut self, parsed: &mut Parsed) -> Result<(), ParseError> {
+        // Removing P operator
+        self.inc();
+
+        let mut duration: ParsedDuration = ParsedDuration::new();
+        let mut got_t: bool = false;
+        let mut last_had_fraction = false;
+
+        loop {
+            match self.current {
+                'T' => {
+                    if got_t {
+                        return Err(
+                            self.parse_error("Repeated time declaration in duration".to_string())
+                        );
+                    }
+
+                    got_t = true;
+                }
+                _c => {
+                    let (value, op_fraction) = self.parse_duration_number_frac()?;
+                    if last_had_fraction {
+                        return Err(self.parse_error("Invalid duration fraction".to_string()));
+                    }
+
+                    if op_fraction.is_some() {
+                        last_had_fraction = true;
+                    }
+
+                    if got_t {
+                        match self.current {
+                            'H' => {
+                                if duration.minutes != 0
+                                    || duration.seconds != 0
+                                    || duration.microseconds != 0
+                                {
+                                    return Err(
+                                        self.parse_error("Duration units out of order".to_string())
+                                    );
+                                }
+
+                                duration.hours += value;
+
+                                if let Some(fraction) = op_fraction {
+                                    let extra_minutes = fraction * 60_f64;
+                                    let extra_full_minutes: f64 = extra_minutes.trunc();
+                                    duration.minutes += extra_full_minutes as u32;
+                                    let extra_seconds =
+                                        ((extra_minutes - extra_full_minutes) * 60.0).round();
+                                    let extra_full_seconds = extra_seconds.trunc();
+                                    duration.seconds += extra_full_seconds as u32;
+                                    let micro_extra = ((extra_seconds - extra_full_seconds)
+                                        * 1_000_000.0)
+                                        .round()
+                                        as u32;
+                                    duration.microseconds += micro_extra;
+                                }
+                            }
+                            'M' => {
+                                if duration.seconds != 0 || duration.microseconds != 0 {
+                                    return Err(
+                                        self.parse_error("Duration units out of order".to_string())
+                                    );
+                                }
+
+                                duration.minutes += value;
+
+                                if let Some(fraction) = op_fraction {
+                                    let extra_seconds = fraction * 60_f64;
+                                    let extra_full_seconds = extra_seconds.trunc();
+                                    duration.seconds += extra_full_seconds as u32;
+                                    let micro_extra = ((extra_seconds - extra_full_seconds)
+                                        * 1_000_000.0)
+                                        .round()
+                                        as u32;
+                                    duration.microseconds += micro_extra;
+                                }
+                            }
+                            'S' => {
+                                duration.seconds = value;
+
+                                if let Some(fraction) = op_fraction {
+                                    duration.microseconds +=
+                                        (fraction * 1_000_000.0).round() as u32;
+                                }
+                            }
+                            _ => {
+                                return Err(
+                                    self.parse_error("Invalid duration time unit".to_string())
+                                )
+                            }
+                        }
+                    } else {
+                        match self.current {
+                            'Y' => {
+                                if last_had_fraction {
+                                    return Err(self.parse_error(
+                                        "Fractional years in duration are not supported"
+                                            .to_string(),
+                                    ));
+                                }
+
+                                if duration.months != 0 || duration.days != 0 {
+                                    return Err(
+                                        self.parse_error("Duration units out of order".to_string())
+                                    );
+                                }
+
+                                duration.years = value;
+                            }
+                            'M' => {
+                                if last_had_fraction {
+                                    return Err(self.parse_error(
+                                        "Fractional months in duration are not supported"
+                                            .to_string(),
+                                    ));
+                                }
+
+                                if duration.days != 0 {
+                                    return Err(
+                                        self.parse_error("Duration units out of order".to_string())
+                                    );
+                                }
+
+                                duration.months = value;
+                            }
+                            'W' => {
+                                if duration.years != 0 || duration.months != 0 {
+                                    return Err(self.parse_error(
+                                        "Basic format durations cannot have weeks".to_string(),
+                                    ));
+                                }
+
+                                duration.weeks = value;
+
+                                if let Some(fraction) = op_fraction {
+                                    let extra_days = fraction * 7_f64;
+                                    let extra_full_days = extra_days.trunc();
+                                    duration.days += extra_full_days as u32;
+                                    let extra_hours = (extra_days - extra_full_days) * 24.0;
+                                    let extra_full_hours = extra_hours.trunc();
+                                    duration.hours += extra_full_hours as u32;
+                                    let extra_minutes =
+                                        ((extra_hours - extra_full_hours) * 60.0).round();
+                                    let extra_full_minutes: f64 = extra_minutes.trunc();
+                                    duration.minutes += extra_full_minutes as u32;
+                                    let extra_seconds =
+                                        ((extra_minutes - extra_full_minutes) * 60.0).round();
+                                    let extra_full_seconds = extra_seconds.trunc();
+                                    duration.seconds += extra_full_seconds as u32;
+                                    let micro_extra = ((extra_seconds - extra_full_seconds)
+                                        * 1_000_000.0)
+                                        .round()
+                                        as u32;
+                                    duration.microseconds += micro_extra;
+                                }
+                            }
+                            'D' => {
+                                if duration.weeks != 0 {
+                                    return Err(self.parse_error(
+                                        "Week format durations cannot have days".to_string(),
+                                    ));
+                                }
+
+                                duration.days += value;
+                                if let Some(fraction) = op_fraction {
+                                    let extra_hours = fraction * 24.0;
+                                    let extra_full_hours = extra_hours.trunc();
+                                    duration.hours += extra_full_hours as u32;
+                                    let extra_minutes =
+                                        ((extra_hours - extra_full_hours) * 60.0).round();
+                                    let extra_full_minutes: f64 = extra_minutes.trunc();
+                                    duration.minutes += extra_full_minutes as u32;
+                                    let extra_seconds =
+                                        ((extra_minutes - extra_full_minutes) * 60.0).round();
+                                    let extra_full_seconds = extra_seconds.trunc();
+                                    duration.seconds += extra_full_seconds as u32;
+                                    let micro_extra = ((extra_seconds - extra_full_seconds)
+                                        * 1_000_000.0)
+                                        .round()
+                                        as u32;
+                                    duration.microseconds += micro_extra;
+                                }
+                            }
+                            _ => {
+                                return Err(
+                                    self.parse_error("Invalid duration time unit".to_string())
+                                )
+                            }
+                        }
+                    }
+                }
+            }
+            self.inc();
+
+            if self.end() {
+                break;
+            }
+        }
+
+        parsed.duration = Some(duration);
+
+        Ok(())
+    }
+
+    fn parse_duration_number_frac(&mut self) -> Result<(u32, Option<f64>), ParseError> {
+        let value = self.parse_duration_number()?;
+        let fraction = matches!(self.current, '.' | ',').then(|| {
+            let mut decimal = 0_f64;
+            let mut denominator = 1_f64;
+
+            while let Some(digit) = self.inc().and_then(|ch| ch.to_digit(10)) {
+                decimal *= 10.0;
+                decimal += f64::from(digit);
+                denominator *= 10.0;
+            }
+
+            decimal / denominator
+        });
+
+        Ok((value, fraction))
+    }
+
+    fn parse_duration_number(&mut self) -> Result<u32, ParseError> {
+        let Some(mut value) = self.current.to_digit(10) else {
+            return Err(self.parse_error("Invalid number in duration".to_string()));
+        };
+
+        while let Some(digit) = self.inc().and_then(|ch| ch.to_digit(10)) {
+            value *= 10;
+            value += digit;
+        }
+
+        Ok(value)
+    }
+
+    fn iso_to_ymd(
+        &mut self,
+        iso_year: u32,
+        iso_week: u32,
+        iso_day: u32,
+    ) -> Result<(u32, u32, u32), ParseError> {
+        if iso_week > 53 || iso_week > 52 && !is_long_year(iso_year as i32) {
+            return Err(ParseError {
+                index: self.idx,
+                message: format!(
+                    "Invalid ISO date: week {iso_week} out of range for year {iso_year}"
+                ),
+            });
+        }
+
+        if iso_day > 7 {
+            return Err(ParseError {
+                index: self.idx,
+                message: "Invalid ISO date: week day is invalid".to_string(),
+            });
+        }
+
+        let ordinal: i32 =
+            iso_week as i32 * 7 + iso_day as i32 - (week_day(iso_year as i32, 1, 4) as i32 + 3);
+
+        self.ordinal_to_ymd(iso_year, ordinal, true)
+    }
+
+    fn ordinal_to_ymd(
+        &mut self,
+        year: u32,
+        ordinal: i32,
+        allow_out_of_bounds: bool,
+    ) -> Result<(u32, u32, u32), ParseError> {
+        let mut ord: i32 = ordinal;
+        let mut y: u32 = year;
+        let mut leap: usize = usize::from(is_leap(y as i32));
+
+        if ord < 1 {
+            if !allow_out_of_bounds {
+                return Err(self.parse_error(format!(
+                    "Invalid ordinal day: {ordinal} is too small for year {year}"
+                )));
+            }
+            // Previous year
+            ord += days_in_year((year - 1) as i32) as i32;
+            y -= 1;
+            leap = usize::from(is_leap(y as i32));
+        }
+
+        if ord > days_in_year(y as i32) as i32 {
+            if !allow_out_of_bounds {
+                return Err(self.parse_error(format!(
+                    "Invalid ordinal day: {ordinal} is too large for year {year}"
+                )));
+            }
+
+            // Next year
+            ord -= days_in_year(y as i32) as i32;
+            y += 1;
+            leap = usize::from(is_leap(y as i32));
+        }
+
+        for i in 1..14 {
+            if ord < MONTHS_OFFSETS[leap][i] {
+                let day = ord as u32 - MONTHS_OFFSETS[leap][i - 1] as u32;
+                let month = (i - 1) as u32;
+
+                return Ok((y, month, day));
+            }
+        }
+
+        Err(self.parse_error(format!(
+            "Invalid ordinal day: {ordinal} is too large for year {year}"
+        )))
+    }
+}
diff --git a/rust/python/helpers.rs b/rust/python/helpers.rs
new file mode 100644
index 0000000..4a53e59
--- /dev/null
+++ b/rust/python/helpers.rs
@@ -0,0 +1,388 @@
+use std::cmp::Ordering;
+
+use pyo3::{
+    intern,
+    prelude::*,
+    types::{PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyString, PyTimeAccess},
+    PyTypeInfo,
+};
+
+use crate::{
+    constants::{DAYS_PER_MONTHS, SECS_PER_DAY, SECS_PER_HOUR, SECS_PER_MIN},
+    helpers,
+};
+
+use crate::python::types::PreciseDiff;
+
+struct DateTimeInfo<'py> {
+    pub year: i32,
+    pub month: i32,
+    pub day: i32,
+    pub hour: i32,
+    pub minute: i32,
+    pub second: i32,
+    pub microsecond: i32,
+    pub total_seconds: i32,
+    pub offset: i32,
+    pub tz: &'py str,
+    pub is_datetime: bool,
+}
+
+impl PartialEq for DateTimeInfo<'_> {
+    fn eq(&self, other: &Self) -> bool {
+        (
+            self.year,
+            self.month,
+            self.day,
+            self.hour,
+            self.minute,
+            self.second,
+            self.microsecond,
+        )
+            .eq(&(
+                other.year,
+                other.month,
+                other.day,
+                other.hour,
+                other.minute,
+                other.second,
+                other.microsecond,
+            ))
+    }
+}
+
+impl PartialOrd for DateTimeInfo<'_> {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        (
+            self.year,
+            self.month,
+            self.day,
+            self.hour,
+            self.minute,
+            self.second,
+            self.microsecond,
+        )
+            .partial_cmp(&(
+                other.year,
+                other.month,
+                other.day,
+                other.hour,
+                other.minute,
+                other.second,
+                other.microsecond,
+            ))
+    }
+}
+
+pub fn get_tz_name<'py>(py: Python, dt: &'py PyAny) -> PyResult<&'py str> {
+    let tz: &str = "";
+
+    if !PyDateTime::is_type_of(dt) {
+        return Ok(tz);
+    }
+
+    let tzinfo = dt.getattr("tzinfo");
+
+    match tzinfo {
+        Err(_) => Ok(tz),
+        Ok(tzinfo) => {
+            if tzinfo.is_none() {
+                return Ok(tz);
+            }
+            if tzinfo.hasattr(intern!(py, "key")).unwrap_or(false) {
+                // zoneinfo timezone
+                let tzname: &PyString = tzinfo
+                    .getattr(intern!(py, "key"))
+                    .unwrap()
+                    .downcast()
+                    .unwrap();
+
+                return tzname.to_str();
+            } else if tzinfo.hasattr(intern!(py, "name")).unwrap_or(false) {
+                // Pendulum timezone
+                let tzname: &PyString = tzinfo
+                    .getattr(intern!(py, "name"))
+                    .unwrap()
+                    .downcast()
+                    .unwrap();
+
+                return tzname.to_str();
+            } else if tzinfo.hasattr(intern!(py, "zone")).unwrap_or(false) {
+                // pytz timezone
+                let tzname: &PyString = tzinfo
+                    .getattr(intern!(py, "zone"))
+                    .unwrap()
+                    .downcast()
+                    .unwrap();
+
+                return tzname.to_str();
+            }
+
+            Ok(tz)
+        }
+    }
+}
+
+pub fn get_offset(dt: &PyAny) -> PyResult<i32> {
+    if !PyDateTime::is_type_of(dt) {
+        return Ok(0);
+    }
+
+    let tzinfo = dt.getattr("tzinfo")?;
+
+    if tzinfo.is_none() {
+        return Ok(0);
+    }
+
+    let offset: &PyDelta = tzinfo.call_method1("utcoffset", (dt,))?.downcast()?;
+
+    Ok(offset.get_days() * SECS_PER_DAY as i32 + offset.get_seconds())
+}
+
+#[pyfunction]
+pub fn is_leap(year: i32) -> PyResult<bool> {
+    Ok(helpers::is_leap(year))
+}
+
+#[pyfunction]
+pub fn is_long_year(year: i32) -> PyResult<bool> {
+    Ok(helpers::is_long_year(year))
+}
+
+#[pyfunction]
+pub fn week_day(year: i32, month: u32, day: u32) -> PyResult<u32> {
+    Ok(helpers::week_day(year, month, day))
+}
+
+#[pyfunction]
+pub fn days_in_year(year: i32) -> PyResult<u32> {
+    Ok(helpers::days_in_year(year))
+}
+
+#[pyfunction]
+pub fn local_time(
+    unix_time: f64,
+    utc_offset: isize,
+    microsecond: usize,
+) -> PyResult<(usize, usize, usize, usize, usize, usize, usize)> {
+    Ok(helpers::local_time(unix_time, utc_offset, microsecond))
+}
+
+#[pyfunction]
+pub fn precise_diff<'py>(py: Python, dt1: &'py PyAny, dt2: &'py PyAny) -> PyResult<PreciseDiff> {
+    let mut sign = 1;
+    let mut dtinfo1 = DateTimeInfo {
+        year: dt1.downcast::<PyDate>()?.get_year(),
+        month: i32::from(dt1.downcast::<PyDate>()?.get_month()),
+        day: i32::from(dt1.downcast::<PyDate>()?.get_day()),
+        hour: 0,
+        minute: 0,
+        second: 0,
+        microsecond: 0,
+        total_seconds: 0,
+        tz: get_tz_name(py, dt1)?,
+        offset: get_offset(dt1)?,
+        is_datetime: PyDateTime::is_type_of(dt1),
+    };
+    let mut dtinfo2 = DateTimeInfo {
+        year: dt2.downcast::<PyDate>()?.get_year(),
+        month: i32::from(dt2.downcast::<PyDate>()?.get_month()),
+        day: i32::from(dt2.downcast::<PyDate>()?.get_day()),
+        hour: 0,
+        minute: 0,
+        second: 0,
+        microsecond: 0,
+        total_seconds: 0,
+        tz: get_tz_name(py, dt2)?,
+        offset: get_offset(dt2)?,
+        is_datetime: PyDateTime::is_type_of(dt2),
+    };
+    let in_same_tz: bool = dtinfo1.tz == dtinfo2.tz && !dtinfo1.tz.is_empty();
+    let mut total_days = helpers::day_number(dtinfo2.year, dtinfo2.month as u8, dtinfo2.day as u8)
+        - helpers::day_number(dtinfo1.year, dtinfo1.month as u8, dtinfo1.day as u8);
+
+    if dtinfo1.is_datetime {
+        let dt1dt: &PyDateTime = dt1.downcast()?;
+
+        dtinfo1.hour = i32::from(dt1dt.get_hour());
+        dtinfo1.minute = i32::from(dt1dt.get_minute());
+        dtinfo1.second = i32::from(dt1dt.get_second());
+        dtinfo1.microsecond = dt1dt.get_microsecond() as i32;
+
+        if !in_same_tz && dtinfo1.offset != 0 || total_days == 0 {
+            dtinfo1.hour -= dtinfo1.offset / SECS_PER_HOUR as i32;
+            dtinfo1.offset %= SECS_PER_HOUR as i32;
+            dtinfo1.minute -= dtinfo1.offset / SECS_PER_MIN as i32;
+            dtinfo1.offset %= SECS_PER_MIN as i32;
+            dtinfo1.second -= dtinfo1.offset;
+
+            if dtinfo1.second < 0 {
+                dtinfo1.second += 60;
+                dtinfo1.minute -= 1;
+            } else if dtinfo1.second > 60 {
+                dtinfo1.second -= 60;
+                dtinfo1.minute += 1;
+            }
+
+            if dtinfo1.minute < 0 {
+                dtinfo1.minute += 60;
+                dtinfo1.hour -= 1;
+            } else if dtinfo1.minute > 60 {
+                dtinfo1.minute -= 60;
+                dtinfo1.hour += 1;
+            }
+
+            if dtinfo1.hour < 0 {
+                dtinfo1.hour += 24;
+                dtinfo1.day -= 1;
+            } else if dtinfo1.hour > 24 {
+                dtinfo1.hour -= 24;
+                dtinfo1.day += 1;
+            }
+        }
+
+        dtinfo1.total_seconds = dtinfo1.hour * SECS_PER_HOUR as i32
+            + dtinfo1.minute * SECS_PER_MIN as i32
+            + dtinfo1.second;
+    }
+
+    if dtinfo2.is_datetime {
+        let dt2dt: &PyDateTime = dt2.downcast()?;
+
+        dtinfo2.hour = i32::from(dt2dt.get_hour());
+        dtinfo2.minute = i32::from(dt2dt.get_minute());
+        dtinfo2.second = i32::from(dt2dt.get_second());
+        dtinfo2.microsecond = dt2dt.get_microsecond() as i32;
+
+        if !in_same_tz && dtinfo2.offset != 0 || total_days == 0 {
+            dtinfo2.hour -= dtinfo2.offset / SECS_PER_HOUR as i32;
+            dtinfo2.offset %= SECS_PER_HOUR as i32;
+            dtinfo2.minute -= dtinfo2.offset / SECS_PER_MIN as i32;
+            dtinfo2.offset %= SECS_PER_MIN as i32;
+            dtinfo2.second -= dtinfo2.offset;
+
+            if dtinfo2.second < 0 {
+                dtinfo2.second += 60;
+                dtinfo2.minute -= 1;
+            } else if dtinfo2.second > 60 {
+                dtinfo2.second -= 60;
+                dtinfo2.minute += 1;
+            }
+
+            if dtinfo2.minute < 0 {
+                dtinfo2.minute += 60;
+                dtinfo2.hour -= 1;
+            } else if dtinfo2.minute > 60 {
+                dtinfo2.minute -= 60;
+                dtinfo2.hour += 1;
+            }
+
+            if dtinfo2.hour < 0 {
+                dtinfo2.hour += 24;
+                dtinfo2.day -= 1;
+            } else if dtinfo2.hour > 24 {
+                dtinfo2.hour -= 24;
+                dtinfo2.day += 1;
+            }
+        }
+
+        dtinfo2.total_seconds = dtinfo2.hour * SECS_PER_HOUR as i32
+            + dtinfo2.minute * SECS_PER_MIN as i32
+            + dtinfo2.second;
+    }
+
+    if dtinfo1 > dtinfo2 {
+        sign = -1;
+        (dtinfo1, dtinfo2) = (dtinfo2, dtinfo1);
+
+        total_days = -total_days;
+    }
+
+    let mut year_diff = dtinfo2.year - dtinfo1.year;
+    let mut month_diff = dtinfo2.month - dtinfo1.month;
+    let mut day_diff = dtinfo2.day - dtinfo1.day;
+    let mut hour_diff = dtinfo2.hour - dtinfo1.hour;
+    let mut minute_diff = dtinfo2.minute - dtinfo1.minute;
+    let mut second_diff = dtinfo2.second - dtinfo1.second;
+    let mut microsecond_diff = dtinfo2.microsecond - dtinfo1.microsecond;
+
+    if microsecond_diff < 0 {
+        microsecond_diff += 1_000_000;
+        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
+        let mut year = dtinfo2.year;
+        let mut month = dtinfo2.month;
+
+        if month == 1 {
+            month = 12;
+            year -= 1;
+        } else {
+            month -= 1;
+        }
+
+        let leap = helpers::is_leap(year);
+
+        let days_in_last_month = DAYS_PER_MONTHS[usize::from(leap)][month as usize];
+        let days_in_month =
+            DAYS_PER_MONTHS[usize::from(helpers::is_leap(dtinfo2.year))][dtinfo2.month as usize];
+
+        match day_diff.cmp(&(days_in_month - days_in_last_month)) {
+            Ordering::Less => {
+                // We don't have a full month, we calculate days
+                if days_in_last_month < dtinfo1.day {
+                    day_diff += dtinfo1.day;
+                } else {
+                    day_diff += days_in_last_month;
+                }
+            }
+            Ordering::Equal => {
+                // We have exactly a full month
+                // We remove the days difference
+                // and add one to the months difference
+                day_diff = 0;
+                month_diff += 1;
+            }
+            Ordering::Greater => {
+                // We have a full month
+                day_diff += days_in_last_month;
+            }
+        }
+
+        month_diff -= 1;
+    }
+
+    if month_diff < 0 {
+        month_diff += 12;
+        year_diff -= 1;
+    }
+
+    Ok(PreciseDiff {
+        years: year_diff * sign,
+        months: month_diff * sign,
+        days: day_diff * sign,
+        hours: hour_diff * sign,
+        minutes: minute_diff * sign,
+        seconds: second_diff * sign,
+        microseconds: microsecond_diff * sign,
+        total_days: total_days * sign,
+    })
+}
diff --git a/rust/python/mod.rs b/rust/python/mod.rs
new file mode 100644
index 0000000..8d3cd41
--- /dev/null
+++ b/rust/python/mod.rs
@@ -0,0 +1,27 @@
+use pyo3::prelude::*;
+
+mod helpers;
+mod parsing;
+mod types;
+
+use helpers::{days_in_year, is_leap, is_long_year, local_time, precise_diff, week_day};
+use parsing::parse_iso8601;
+use types::{Duration, PreciseDiff};
+
+#[pymodule]
+pub fn _pendulum(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
+    m.add_function(wrap_pyfunction!(days_in_year, m)?)?;
+    m.add_function(wrap_pyfunction!(is_leap, m)?)?;
+    m.add_function(wrap_pyfunction!(is_long_year, m)?)?;
+    m.add_function(wrap_pyfunction!(local_time, m)?)?;
+    m.add_function(wrap_pyfunction!(week_day, m)?)?;
+    m.add_function(wrap_pyfunction!(parse_iso8601, m)?)?;
+    m.add_function(wrap_pyfunction!(precise_diff, m)?)?;
+    m.add_class::<Duration>()?;
+    m.add_class::<PreciseDiff>()?;
+
+    #[cfg(not(feature = "mimalloc"))]
+    m.setattr("__pendulum_default_allocator__", true)?; // uses setattr so this is not in __all__
+
+    Ok(())
+}
diff --git a/rust/python/parsing.rs b/rust/python/parsing.rs
new file mode 100644
index 0000000..48fa64c
--- /dev/null
+++ b/rust/python/parsing.rs
@@ -0,0 +1,117 @@
+use pyo3::exceptions;
+use pyo3::prelude::*;
+use pyo3::types::PyDate;
+use pyo3::types::PyDateTime;
+use pyo3::types::PyTime;
+
+use crate::parsing::Parser;
+use crate::python::types::{Duration, FixedTimezone};
+
+#[pyfunction]
+pub fn parse_iso8601(py: Python, input: &str) -> PyResult<PyObject> {
+    let parsed = Parser::new(input).parse();
+
+    match parsed {
+        Ok(parsed) => match (parsed.datetime, parsed.duration, parsed.second_datetime) {
+            (Some(datetime), None, None) => match (datetime.has_date, datetime.has_time) {
+                (true, true) => match datetime.offset {
+                    Some(offset) => {
+                        let dt = PyDateTime::new(
+                            py,
+                            datetime.year as i32,
+                            datetime.month as u8,
+                            datetime.day as u8,
+                            datetime.hour as u8,
+                            datetime.minute as u8,
+                            datetime.second as u8,
+                            datetime.microsecond,
+                            Some(
+                                Py::new(py, FixedTimezone::new(offset, datetime.tzname))?
+                                    .to_object(py)
+                                    .extract(py)?,
+                            ),
+                        )?;
+
+                        Ok(dt.to_object(py))
+                    }
+                    None => {
+                        let dt = PyDateTime::new(
+                            py,
+                            datetime.year as i32,
+                            datetime.month as u8,
+                            datetime.day as u8,
+                            datetime.hour as u8,
+                            datetime.minute as u8,
+                            datetime.second as u8,
+                            datetime.microsecond,
+                            None,
+                        )?;
+
+                        Ok(dt.to_object(py))
+                    }
+                },
+                (true, false) => {
+                    let dt = PyDate::new(
+                        py,
+                        datetime.year as i32,
+                        datetime.month as u8,
+                        datetime.day as u8,
+                    )?;
+
+                    Ok(dt.to_object(py))
+                }
+                (false, true) => match datetime.offset {
+                    Some(offset) => {
+                        let dt = PyTime::new(
+                            py,
+                            datetime.hour as u8,
+                            datetime.minute as u8,
+                            datetime.second as u8,
+                            datetime.microsecond,
+                            Some(
+                                Py::new(py, FixedTimezone::new(offset, datetime.tzname))?
+                                    .to_object(py)
+                                    .extract(py)?,
+                            ),
+                        )?;
+
+                        Ok(dt.to_object(py))
+                    }
+                    None => {
+                        let dt = PyTime::new(
+                            py,
+                            datetime.hour as u8,
+                            datetime.minute as u8,
+                            datetime.second as u8,
+                            datetime.microsecond,
+                            None,
+                        )?;
+
+                        Ok(dt.to_object(py))
+                    }
+                },
+                (_, _) => Err(exceptions::PyValueError::new_err(
+                    "Parsing error".to_string(),
+                )),
+            },
+            (None, Some(duration), None) => Ok(Py::new(
+                py,
+                Duration::new(
+                    Some(duration.years),
+                    Some(duration.months),
+                    Some(duration.weeks),
+                    Some(duration.days),
+                    Some(duration.hours),
+                    Some(duration.minutes),
+                    Some(duration.seconds),
+                    Some(duration.microseconds),
+                ),
+            )?
+            .to_object(py)),
+            (_, _, _) => Err(exceptions::PyValueError::new_err(
+                "Not yet implemented".to_string(),
+            )),
+        },
+        Err(error) => Err(exceptions::PyValueError::new_err(error.to_string())),
+    }
+}
diff --git a/rust/python/types/duration.rs b/rust/python/types/duration.rs
new file mode 100644
index 0000000..fc18f4e
--- /dev/null
+++ b/rust/python/types/duration.rs
@@ -0,0 +1,59 @@
+use pyo3::prelude::*;
+
+#[pyclass(module = "_pendulum")]
+pub struct Duration {
+    #[pyo3(get, set)]
+    pub years: u32,
+    #[pyo3(get, set)]
+    pub months: u32,
+    #[pyo3(get, set)]
+    pub weeks: u32,
+    #[pyo3(get, set)]
+    pub days: u32,
+    #[pyo3(get, set)]
+    pub hours: u32,
+    #[pyo3(get, set)]
+    pub minutes: u32,
+    #[pyo3(get, set)]
+    pub seconds: u32,
+    #[pyo3(get, set)]
+    pub microseconds: u32,
+}
+
+#[pymethods]
+impl Duration {
+    #[new]
+    #[pyo3(signature = (years=0, months=0, weeks=0, days=0, hours=0, minutes=0, seconds=0, microseconds=0))]
+    #[allow(clippy::too_many_arguments)]
+    pub fn new(
+        years: Option<u32>,
+        months: Option<u32>,
+        weeks: Option<u32>,
+        days: Option<u32>,
+        hours: Option<u32>,
+        minutes: Option<u32>,
+        seconds: Option<u32>,
+        microseconds: Option<u32>,
+    ) -> Self {
+        Self {
+            years: years.unwrap_or(0),
+            months: months.unwrap_or(0),
+            weeks: weeks.unwrap_or(0),
+            days: days.unwrap_or(0),
+            hours: hours.unwrap_or(0),
+            minutes: minutes.unwrap_or(0),
+            seconds: seconds.unwrap_or(0),
+            microseconds: microseconds.unwrap_or(0),
+        }
+    }
+
+    #[getter]
+    fn remaining_days(&self) -> PyResult<u32> {
+        Ok(self.days)
+    }
+
+    #[getter]
+    fn remaining_seconds(&self) -> PyResult<u32> {
+        Ok(self.seconds)
+    }
+}
diff --git a/rust/python/types/mod.rs b/rust/python/types/mod.rs
new file mode 100644
index 0000000..cba11df
--- /dev/null
+++ b/rust/python/types/mod.rs
@@ -0,0 +1,7 @@
+mod duration;
+mod precise_diff;
+mod timezone;
+
+pub use duration::Duration;
+pub use precise_diff::PreciseDiff;
+pub use timezone::FixedTimezone;
diff --git a/rust/python/types/precise_diff.rs b/rust/python/types/precise_diff.rs
new file mode 100644
index 0000000..64ca3a6
--- /dev/null
+++ b/rust/python/types/precise_diff.rs
@@ -0,0 +1,53 @@
+use pyo3::prelude::*;
+
+#[pyclass(module = "_pendulum")]
+pub struct PreciseDiff {
+    #[pyo3(get, set)]
+    pub years: i32,
+    #[pyo3(get, set)]
+    pub months: i32,
+    #[pyo3(get, set)]
+    pub days: i32,
+    #[pyo3(get, set)]
+    pub hours: i32,
+    #[pyo3(get, set)]
+    pub minutes: i32,
+    #[pyo3(get, set)]
+    pub seconds: i32,
+    #[pyo3(get, set)]
+    pub microseconds: i32,
+    #[pyo3(get, set)]
+    pub total_days: i32,
+}
+
+#[pymethods]
+impl PreciseDiff {
+    #[new]
+    #[pyo3(signature = (years=0, months=0, days=0, hours=0, minutes=0, seconds=0, microseconds=0, total_days=0))]
+    #[allow(clippy::too_many_arguments)]
+    pub fn new(
+        years: Option<i32>,
+        months: Option<i32>,
+        days: Option<i32>,
+        hours: Option<i32>,
+        minutes: Option<i32>,
+        seconds: Option<i32>,
+        microseconds: Option<i32>,
+        total_days: Option<i32>,
+    ) -> Self {
+        Self {
+            years: years.unwrap_or(0),
+            months: months.unwrap_or(0),
+            days: days.unwrap_or(0),
+            hours: hours.unwrap_or(0),
+            minutes: minutes.unwrap_or(0),
+            seconds: seconds.unwrap_or(0),
+            microseconds: microseconds.unwrap_or(0),
+            total_days: total_days.unwrap_or(0),
+        }
+    }
+
+    fn __repr__(&self) -> String {
+        format!("PreciseDiff(years={}, months={}, days={}, hours={}, minutes={}, seconds={}, microseconds={}, total_days={})", self.years, self.months, self.days, self.hours, self.minutes, self.seconds, self.microseconds, self.total_days)
+    }
+}
diff --git a/rust/python/types/timezone.rs b/rust/python/types/timezone.rs
new file mode 100644
index 0000000..1a8bbad
--- /dev/null
+++ b/rust/python/types/timezone.rs
@@ -0,0 +1,52 @@
+use pyo3::prelude::*;
+use pyo3::types::{PyDelta, PyDict, PyTzInfo};
+
+#[pyclass(module = "_pendulum", extends = PyTzInfo)]
+#[derive(Clone)]
+pub struct FixedTimezone {
+    offset: i32,
+    name: Option<String>,
+}
+
+#[pymethods]
+impl FixedTimezone {
+    #[new]
+    pub fn new(offset: i32, name: Option<String>) -> Self {
+        Self { offset, name }
+    }
+
+    fn utcoffset<'p>(&self, py: Python<'p>, _dt: &PyAny) -> PyResult<&'p PyDelta> {
+        PyDelta::new(py, 0, self.offset, 0, true)
+    }
+
+    fn tzname(&self, _dt: &PyAny) -> String {
+        self.__str__()
+    }
+
+    fn dst<'p>(&self, py: Python<'p>, _dt: &PyAny) -> PyResult<&'p PyDelta> {
+        PyDelta::new(py, 0, 0, 0, true)
+    }
+
+    fn __repr__(&self) -> String {
+        format!(
+            "FixedTimezone({}, name=\"{}\")",
+            self.offset,
+            self.__str__()
+        )
+    }
+
+    fn __str__(&self) -> String {
+        if let Some(n) = &self.name {
+            n.clone()
+        } else {
+            let sign = if self.offset < 0 { "-" } else { "+" };
+            let minutes = self.offset.abs() / 60;
+            let (hour, minute) = (minutes / 60, minutes % 60);
+            format!("{sign}{hour:.2}:{minute:.2}")
+        }
+    }
+
+    fn __deepcopy__(&self, py: Python, _memo: &PyDict) -> PyResult<Py<Self>> {
+        Py::new(py, self.clone())
+    }
+}


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index ddbb21a..e8c604f 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -13,6 +13,20 @@ on:
       - '**'
 
 jobs:
+  Linting:
+    name: Linting
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+        with:
+          python-version: "3.11"
+      - name: "Install pre-commit"
+        run: pip install pre-commit
+      - name: "Install Rust toolchain"
+        run: rustup component add rustfmt clippy
+      - run: pre-commit run --all-files
+
   Tests:
     name: ${{ matrix.os }} / ${{ matrix.python-version }}
     runs-on: ${{ matrix.os }}-latest
diff --git a/tests/datetime/test_from_format.py b/tests/datetime/test_from_format.py
index 7b498b3..1d5e24a 100644
--- a/tests/datetime/test_from_format.py
+++ b/tests/datetime/test_from_format.py
@@ -81,9 +81,8 @@ def test_from_format_with_invalid_padded_day():
         ("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),
+        ("2-2-99", "D-M-YY", "1999-02-02T00:00:00+00:00", None),
+        ("99", "YY", "1999-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),
@@ -198,3 +197,25 @@ def test_strptime():
     assert_datetime(d, 1975, 5, 21, 22, 32, 11)
     assert isinstance(d, pendulum.DateTime)
     assert d.timezone_name == "UTC"
+
+
+def test_from_format_2_digit_year():
+    """
+    Complies with open group spec for 2 digit years
+    https://pubs.opengroup.org/onlinepubs/9699919799/
+
+    "If century is not specified, then values in the range [69,99] shall
+    refer to years 1969 to 1999 inclusive, and values in the
+    range [00,68] shall refer to years 2000 to 2068 inclusive."
+    """
+    d = pendulum.from_format("00", "YY")
+    assert d.year == 2000
+
+    d = pendulum.from_format("68", "YY")
+    assert d.year == 2068
+
+    d = pendulum.from_format("69", "YY")
+    assert d.year == 1969
+
+    d = pendulum.from_format("99", "YY")
+    assert d.year == 1999
diff --git a/tests/interval/test_behavior.py b/tests/interval/test_behavior.py
index b5e057a..29636bd 100644
--- a/tests/interval/test_behavior.py
+++ b/tests/interval/test_behavior.py
@@ -52,3 +52,16 @@ def test_equality_to_timedelta():
     period = dt2 - dt1
 
     assert period == timedelta(days=2)
+
+
+def test_inequality():
+    dt1 = pendulum.datetime(2016, 11, 18)
+    dt2 = pendulum.datetime(2016, 11, 20)
+    dt3 = pendulum.datetime(2016, 11, 22)
+
+    period1 = dt2 - dt1
+    period2 = dt3 - dt2
+    period3 = dt3 - dt1
+
+    assert period1 != period2
+    assert period1 != period3
diff --git a/tests/localization/test_tr.py b/tests/localization/test_tr.py
index 5f77abc..258a5de 100644
--- a/tests/localization/test_tr.py
+++ b/tests/localization/test_tr.py
@@ -7,7 +7,7 @@ locale = "tr"
 
 
 def test_diff_for_humans():
-    with pendulum.test(pendulum.datetime(2016, 8, 29)):
+    with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True):
         diff_for_humans()
 
 
@@ -49,10 +49,10 @@ def diff_for_humans():
     assert d.diff_for_humans(locale=locale) == "2 ay önce"
 
     d = pendulum.now().subtract(years=1)
-    assert d.diff_for_humans(locale=locale) == "1 yıl önce"
+    assert d.diff_for_humans(locale=locale) == "1 yıl önce"  # noqa: RUF001
 
     d = pendulum.now().subtract(years=2)
-    assert d.diff_for_humans(locale=locale) == "2 yıl önce"
+    assert d.diff_for_humans(locale=locale) == "2 yıl önce"  # noqa: RUF001
 
     d = pendulum.now().add(seconds=1)
     assert d.diff_for_humans(locale=locale) == "1 saniye sonra"
diff --git a/tests/parsing/test_parse_iso8601.py b/tests/parsing/test_parse_iso8601.py
index 83f2881..22c43a2 100644
--- a/tests/parsing/test_parse_iso8601.py
+++ b/tests/parsing/test_parse_iso8601.py
@@ -10,82 +10,84 @@ from pendulum.parsing import parse_iso8601
 
 
 try:
-    from pendulum.parsing._extension import TZFixedOffset as FixedTimezone
+    from _pendulum import 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")
+@pytest.mark.parametrize(
+    ["text", "expected"],
+    [
+        ("2016-10", date(2016, 10, 1)),
+        ("2016-10-06", date(2016, 10, 6)),
+        # Ordinal date
+        ("2012-007", date(2012, 1, 7)),
+        ("2012007", date(2012, 1, 7)),
+        ("2017-079", date(2017, 3, 20)),
+        # Week date
+        ("2012-W05", date(2012, 1, 30)),
+        ("2008-W39-6", date(2008, 9, 27)),
+        ("2009-W53-7", date(2010, 1, 3)),
+        ("2009-W01-1", date(2008, 12, 29)),
+        # Time
+        ("12:34", time(12, 34, 0)),
+        ("12:34:56", time(12, 34, 56)),
+        ("12:34:56.123", time(12, 34, 56, 123000)),
+        ("12:34:56.123456", time(12, 34, 56, 123456)),
+        ("12:34+05:30", time(12, 34, 0, tzinfo=FixedTimezone(19800))),
+        ("12:34:56+05:30", time(12, 34, 56, tzinfo=FixedTimezone(19800))),
+        ("12:34:56.123+05:30", time(12, 34, 56, 123000, tzinfo=FixedTimezone(19800))),
+        (
+            "12:34:56.123456+05:30",
+            time(12, 34, 56, 123456, tzinfo=FixedTimezone(19800)),
+        ),
+        # Datetime
+        ("2016-10-06T12:34:56.123456", datetime(2016, 10, 6, 12, 34, 56, 123456)),
+        ("2016-10-06T12:34:56.123", datetime(2016, 10, 6, 12, 34, 56, 123000)),
+        ("2016-10-06T12:34:56.000123", datetime(2016, 10, 6, 12, 34, 56, 123)),
+        ("20161006T12", datetime(2016, 10, 6, 12, 0, 0, 0)),
+        ("20161006T123456", datetime(2016, 10, 6, 12, 34, 56, 0)),
+        ("20161006T123456.123456", datetime(2016, 10, 6, 12, 34, 56, 123456)),
+        ("20161006 123456.123456", datetime(2016, 10, 6, 12, 34, 56, 123456)),
+        # Datetime with offset
+        (
+            "2016-10-06T12:34:56.123456+05:30",
+            datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(19800)),
+        ),
+        (
+            "2016-10-06T12:34:56.123456+0530",
+            datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(19800)),
+        ),
+        (
+            "2016-10-06T12:34:56.123456-05:30",
+            datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(-19800)),
+        ),
+        (
+            "2016-10-06T12:34:56.123456-0530",
+            datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(-19800)),
+        ),
+        (
+            "2016-10-06T12:34:56.123456+05",
+            datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(18000)),
+        ),
+        (
+            "2016-10-06T12:34:56.123456-05",
+            datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(-18000)),
+        ),
+        (
+            "20161006T123456,123456-05",
+            datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(-18000)),
+        ),
+        (
+            "2016-10-06T12:34:56.123456789+05:30",
+            datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(+19800)),
+        ),
+        # Week date with time
+        ("2008-W39-6T09", datetime(2008, 9, 27, 9, 0, 0, 0)),
+    ],
+)
+def test_parse_iso8601(text: str, expected: date) -> None:
+    assert parse_iso8601(text) == expected
 
 
 def test_parse_ios8601_invalid():
@@ -166,301 +168,43 @@ def test_parse_ios8601_invalid():
         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")
+@pytest.mark.parametrize(
+    ["text", "expected"],
+    [
+        ("P2Y3M4DT5H6M7S", (2, 3, 0, 4, 5, 6, 7, 0)),
+        ("P1Y2M3DT4H5M6.5S", (1, 2, 0, 3, 4, 5, 6, 500_000)),
+        ("P1Y2M3DT4H5M6,5S", (1, 2, 0, 3, 4, 5, 6, 500_000)),
+        ("P1Y2M3D", (1, 2, 0, 3, 0, 0, 0, 0)),
+        ("P1Y2M3.5D", (1, 2, 0, 3, 12, 0, 0, 0)),
+        ("P1Y2M3,5D", (1, 2, 0, 3, 12, 0, 0, 0)),
+        ("PT4H54M6.5S", (0, 0, 0, 0, 4, 54, 6, 500_000)),
+        ("PT4H54M6,5S", (0, 0, 0, 0, 4, 54, 6, 500_000)),
+        ("P1Y", (1, 0, 0, 0, 0, 0, 0, 0)),
+        ("P1M", (0, 1, 0, 0, 0, 0, 0, 0)),
+        ("P1W", (0, 0, 1, 0, 0, 0, 0, 0)),
+        ("P1.5W", (0, 0, 1, 3, 12, 0, 0, 0)),
+        ("P1,5W", (0, 0, 1, 3, 12, 0, 0, 0)),
+        ("P1D", (0, 0, 0, 1, 0, 0, 0, 0)),
+        ("P1.5D", (0, 0, 0, 1, 12, 0, 0, 0)),
+        ("P1,5D", (0, 0, 0, 1, 12, 0, 0, 0)),
+        ("PT1H", (0, 0, 0, 0, 1, 0, 0, 0)),
+        ("PT1.5H", (0, 0, 0, 0, 1, 30, 0, 0)),
+        ("PT1,5H", (0, 0, 0, 0, 1, 30, 0, 0)),
+        ("P2Y30M4DT5H6M7S", (2, 30, 0, 4, 5, 6, 7, 0)),
+    ],
+)
+def test_parse_ios8601_duration(
+    text: str, expected: tuple[int, int, int, int, int, int, int, int]
+) -> None:
+    parsed = parse_iso8601(text)
+
+    assert (
+        parsed.years,
+        parsed.months,
+        parsed.weeks,
+        parsed.remaining_days,
+        parsed.hours,
+        parsed.minutes,
+        parsed.remaining_seconds,
+        parsed.microseconds,
+    ) == expected
diff --git a/tests/parsing/test_parsing.py b/tests/parsing/test_parsing.py
index 35dcf86..d57b82f 100644
--- a/tests/parsing/test_parsing.py
+++ b/tests/parsing/test_parsing.py
@@ -276,19 +276,6 @@ def test_iso8601_datetime():
     assert parsed.microsecond == 0
     assert parsed.utcoffset().total_seconds() == 19800
 
-    text = "20161001T1430,4+0530"
-
-    parsed = parse(text)
-
-    assert parsed.year == 2016
-    assert parsed.month == 10
-    assert parsed.day == 1
-    assert parsed.hour == 14
-    assert parsed.minute == 30
-    assert parsed.second == 0
-    assert parsed.microsecond == 400000
-    assert parsed.utcoffset().total_seconds() == 19800
-
     text = "2008-09-03T20:56:35.450686+01"
 
     parsed = parse(text)
@@ -480,7 +467,7 @@ def test_iso8601_ordinal():
 def test_iso8601_time():
     now = pendulum.datetime(2015, 11, 12)
 
-    text = "201205"
+    text = "T201205"
 
     parsed = parse(text, now=now)
 
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index 7756a06..b9ab63f 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -48,6 +48,7 @@ def test_precise_diff() -> None:
 
     diff = precise_diff(dt1, dt2)
     assert_diff(diff, months=-1, seconds=-1)
+    assert diff.total_days == -30
 
     diff = precise_diff(dt2, dt1)
     assert_diff(diff, months=1, seconds=1)
@@ -93,11 +94,13 @@ def test_precise_diff_timezone() -> None:
 
     diff = precise_diff(dt1, dt2)
     assert_diff(diff, days=1, hours=0)
+    assert diff.total_days == 1
 
     dt2 = toronto.datetime(2013, 4, 1, 1, 30)
 
     diff = precise_diff(dt1, dt2)
     assert_diff(diff, days=1, hours=5)
+    assert diff.total_days == 1
 
     # pytz
     paris_pytz = pytz.timezone("Europe/Paris")
@@ -108,6 +111,7 @@ def test_precise_diff_timezone() -> None:
 
     diff = precise_diff(dt1, dt2)
     assert_diff(diff, days=1, hours=0)
+    assert diff.total_days == 1
 
     dt2 = toronto_pytz.localize(datetime(2013, 4, 1, 1, 30))
 
@@ -119,6 +123,7 @@ def test_precise_diff_timezone() -> None:
     dt2 = timezone("Europe/Paris").datetime(2018, 6, 20, 3, 40)  # UTC+2
     diff = precise_diff(dt1, dt2)
     assert_diff(diff, minutes=10)
+    assert diff.total_days == 0
 
 
 def test_week_day() -> None: