python-dateutil-rs 0.0.15__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/Cargo.lock +2 -13
  2. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/Cargo.toml +1 -1
  3. python_dateutil_rs-0.1.1/PKG-INFO +232 -0
  4. python_dateutil_rs-0.1.1/README.md +209 -0
  5. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/Cargo.toml +1 -1
  6. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/common.rs +36 -49
  7. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/easter.rs +0 -44
  8. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/error.rs +2 -0
  9. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/parser/isoparser.rs +21 -0
  10. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/parser/parserinfo.rs +40 -0
  11. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/parser.rs +368 -82
  12. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/relativedelta.rs +223 -25
  13. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/rrule/iter.rs +30 -27
  14. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/rrule/parse.rs +25 -0
  15. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/rrule/set.rs +38 -0
  16. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/rrule.rs +525 -0
  17. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/tz/local.rs +40 -2
  18. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/tz/offset.rs +12 -0
  19. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/tz.rs +38 -14
  20. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-py/Cargo.toml +1 -2
  21. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/py/common.rs +15 -5
  22. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/py/relativedelta.rs +34 -2
  23. python_dateutil_rs-0.1.1/crates/dateutil-py/src/py/rrule.rs +926 -0
  24. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/py.rs +2 -3
  25. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/pyproject.toml +5 -5
  26. {python_dateutil_rs-0.0.15/python/dateutil_rs/v1 → python_dateutil_rs-0.1.1/python/dateutil_rs}/__init__.py +22 -15
  27. {python_dateutil_rs-0.0.15/python/dateutil_rs/v1 → python_dateutil_rs-0.1.1/python/dateutil_rs}/_native.pyi +34 -17
  28. python_dateutil_rs-0.1.1/python/dateutil_rs/easter.py +15 -0
  29. {python_dateutil_rs-0.0.15/python/dateutil_rs/v1 → python_dateutil_rs-0.1.1/python/dateutil_rs}/parser.py +2 -2
  30. python_dateutil_rs-0.1.1/python/dateutil_rs/relativedelta.py +5 -0
  31. {python_dateutil_rs-0.0.15/python/dateutil_rs/v1 → python_dateutil_rs-0.1.1/python/dateutil_rs}/rrule.py +2 -2
  32. {python_dateutil_rs-0.0.15/python/dateutil_rs/v1 → python_dateutil_rs-0.1.1/python/dateutil_rs}/tz.py +2 -2
  33. python_dateutil_rs-0.0.15/PKG-INFO +0 -309
  34. python_dateutil_rs-0.0.15/README.md +0 -286
  35. python_dateutil_rs-0.0.15/crates/dateutil-py/src/py/rrule.rs +0 -614
  36. python_dateutil_rs-0.0.15/crates/dateutil-rs/Cargo.toml +0 -29
  37. python_dateutil_rs-0.0.15/crates/dateutil-rs/LICENSE +0 -21
  38. python_dateutil_rs-0.0.15/crates/dateutil-rs/README.md +0 -286
  39. python_dateutil_rs-0.0.15/crates/dateutil-rs/benches/benchmarks.rs +0 -349
  40. python_dateutil_rs-0.0.15/crates/dateutil-rs/src/common.rs +0 -180
  41. python_dateutil_rs-0.0.15/crates/dateutil-rs/src/easter.rs +0 -214
  42. python_dateutil_rs-0.0.15/crates/dateutil-rs/src/lib.rs +0 -62
  43. python_dateutil_rs-0.0.15/crates/dateutil-rs/src/parser/isoparser.rs +0 -767
  44. python_dateutil_rs-0.0.15/crates/dateutil-rs/src/parser.rs +0 -2525
  45. python_dateutil_rs-0.0.15/crates/dateutil-rs/src/relativedelta.rs +0 -1667
  46. python_dateutil_rs-0.0.15/crates/dateutil-rs/src/rrule/iter.rs +0 -1417
  47. python_dateutil_rs-0.0.15/crates/dateutil-rs/src/rrule.rs +0 -3651
  48. python_dateutil_rs-0.0.15/crates/dateutil-rs/src/tz/file.rs +0 -819
  49. python_dateutil_rs-0.0.15/crates/dateutil-rs/src/tz/local.rs +0 -239
  50. python_dateutil_rs-0.0.15/crates/dateutil-rs/src/tz/offset.rs +0 -136
  51. python_dateutil_rs-0.0.15/crates/dateutil-rs/src/tz/range.rs +0 -1344
  52. python_dateutil_rs-0.0.15/crates/dateutil-rs/src/tz/utc.rs +0 -102
  53. python_dateutil_rs-0.0.15/crates/dateutil-rs/src/tz.rs +0 -1579
  54. python_dateutil_rs-0.0.15/crates/dateutil-rs/src/utils.rs +0 -141
  55. python_dateutil_rs-0.0.15/python/dateutil_rs/__init__.py +0 -31
  56. python_dateutil_rs-0.0.15/python/dateutil_rs/_native.pyi +0 -397
  57. python_dateutil_rs-0.0.15/python/dateutil_rs/common.py +0 -5
  58. python_dateutil_rs-0.0.15/python/dateutil_rs/easter.py +0 -5
  59. python_dateutil_rs-0.0.15/python/dateutil_rs/parser.py +0 -272
  60. python_dateutil_rs-0.0.15/python/dateutil_rs/relativedelta.py +0 -4
  61. python_dateutil_rs-0.0.15/python/dateutil_rs/rrule.py +0 -46
  62. python_dateutil_rs-0.0.15/python/dateutil_rs/tz.py +0 -476
  63. python_dateutil_rs-0.0.15/python/dateutil_rs/utils.py +0 -5
  64. python_dateutil_rs-0.0.15/python/dateutil_rs/v1/common.py +0 -5
  65. python_dateutil_rs-0.0.15/python/dateutil_rs/v1/easter.py +0 -10
  66. python_dateutil_rs-0.0.15/python/dateutil_rs/v1/py.typed +0 -0
  67. python_dateutil_rs-0.0.15/python/dateutil_rs/v1/relativedelta.py +0 -5
  68. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/LICENSE +0 -0
  69. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/CLAUDE.md +0 -0
  70. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/benches/benchmarks.rs +0 -0
  71. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/lib.rs +0 -0
  72. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/parser/tokenizer.rs +0 -0
  73. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/tz/file.rs +0 -0
  74. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/tz/utc.rs +0 -0
  75. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/lib.rs +0 -0
  76. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/py/conv.rs +0 -0
  77. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/py/easter.rs +0 -0
  78. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/py/parser.rs +0 -0
  79. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/py/tz.rs +0 -0
  80. {python_dateutil_rs-0.0.15 → python_dateutil_rs-0.1.1}/python/dateutil_rs/py.typed +0 -0
@@ -220,7 +220,7 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
220
220
 
221
221
  [[package]]
222
222
  name = "dateutil-core"
223
- version = "0.1.0"
223
+ version = "0.1.1"
224
224
  dependencies = [
225
225
  "bitflags",
226
226
  "chrono",
@@ -233,24 +233,13 @@ dependencies = [
233
233
 
234
234
  [[package]]
235
235
  name = "dateutil-py"
236
- version = "0.1.0"
236
+ version = "0.1.1"
237
237
  dependencies = [
238
238
  "chrono",
239
239
  "dateutil-core",
240
240
  "pyo3",
241
241
  ]
242
242
 
243
- [[package]]
244
- name = "dateutil-rs"
245
- version = "0.0.15"
246
- dependencies = [
247
- "chrono",
248
- "criterion",
249
- "dateutil-py",
250
- "pyo3",
251
- "thiserror",
252
- ]
253
-
254
243
  [[package]]
255
244
  name = "either"
256
245
  version = "1.15.0"
@@ -1,5 +1,5 @@
1
1
  [workspace]
2
- members = ["crates/dateutil-core", "crates/dateutil-py", "crates/dateutil-rs"]
2
+ members = ["crates/dateutil-core", "crates/dateutil-py"]
3
3
  resolver = "2"
4
4
 
5
5
  [profile.release]
@@ -0,0 +1,232 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-dateutil-rs
3
+ Version: 0.1.1
4
+ Classifier: Programming Language :: Python :: 3.10
5
+ Classifier: Programming Language :: Python :: 3.11
6
+ Classifier: Programming Language :: Python :: 3.12
7
+ Classifier: Programming Language :: Python :: 3.13
8
+ Classifier: Programming Language :: Python :: 3.14
9
+ Requires-Dist: python-dateutil>=2.9 ; extra == 'dev'
10
+ Requires-Dist: pytest>=9.0 ; extra == 'dev'
11
+ Requires-Dist: pytest-cov>=7.1 ; extra == 'dev'
12
+ Requires-Dist: pytest-benchmark>=5.2 ; extra == 'dev'
13
+ Requires-Dist: freezegun>=1.5 ; extra == 'dev'
14
+ Requires-Dist: hypothesis>=6.151 ; extra == 'dev'
15
+ Requires-Dist: maturin>=1.13 ; extra == 'dev'
16
+ Provides-Extra: dev
17
+ License-File: LICENSE
18
+ Summary: A Rust-backed port of python-dateutil
19
+ License-Expression: MIT
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
22
+
23
+ # python-dateutil-rs
24
+
25
+ [![PyPI](https://img.shields.io/pypi/v/python-dateutil-rs.svg?style=flat-square)](https://pypi.org/project/python-dateutil-rs/)
26
+ [![Python](https://img.shields.io/pypi/pyversions/python-dateutil-rs.svg?style=flat-square)](https://pypi.org/project/python-dateutil-rs/)
27
+ [![License](https://img.shields.io/pypi/l/python-dateutil-rs.svg?style=flat-square)](https://pypi.org/project/python-dateutil-rs/)
28
+ [![CI](https://github.com/wakita181009/dateutil-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/wakita181009/dateutil-rs/actions/workflows/ci.yml)
29
+ [![Coverage](https://codecov.io/gh/wakita181009/dateutil-rs/branch/main/graph/badge.svg)](https://codecov.io/gh/wakita181009/dateutil-rs)
30
+
31
+ A high-performance Rust-backed port of [python-dateutil](https://github.com/dateutil/dateutil) (v2.9.0).
32
+
33
+ > **Status:** All core modules (easter, relativedelta, parser, rrule, tz) are rewritten in Rust via PyO3/maturin. The optimized `dateutil-core` + `dateutil-py` architecture delivers **2x-897x** speedups over python-dateutil.
34
+
35
+ ## Features
36
+
37
+ - **Drop-in replacement** for `python-dateutil` — same API, same behavior
38
+ - **Rust-accelerated:** easter, relativedelta, parser (`parse` / `isoparse`), rrule, tz, weekday
39
+ - **Optimized core:** zero-copy parser, PHF lookup tables, bitflag filters, buffer-reusing rrule
40
+ - **Comprehensive test suite** inherited from the original project
41
+ - **Benchmark infrastructure** for side-by-side performance comparison
42
+ - **Python 3.10-3.14** supported on Linux and macOS
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install python-dateutil-rs
48
+ ```
49
+
50
+ ## Usage
51
+
52
+ ```python
53
+ from dateutil_rs import (
54
+ parse, isoparse, relativedelta, rrule, rruleset, rrulestr,
55
+ easter, gettz, tzutc, tzoffset,
56
+ MONTHLY, MO, TU, WE, TH, FR, SA, SU,
57
+ )
58
+
59
+ # Parse date strings (zero-copy tokenizer)
60
+ dt = parse("2026-01-15T10:30:00+09:00")
61
+
62
+ # ISO-8601 strict parsing
63
+ dt = isoparse("2026-01-15T10:30:00")
64
+
65
+ # Relative deltas
66
+ next_month = dt + relativedelta(months=+1)
67
+
68
+ # Recurrence rules (buffer-reusing iterator)
69
+ monthly = rrule(MONTHLY, count=5, dtstart=parse("2026-01-01"))
70
+ dates = monthly.all()
71
+ dates = list(monthly) # also iterable
72
+ first = monthly[0] # indexing
73
+ subset = monthly[1:3] # slicing
74
+ n = monthly.count() # total occurrences
75
+ dt in monthly # membership test
76
+
77
+ # Timezones
78
+ tokyo = gettz("Asia/Tokyo")
79
+ utc = tzutc()
80
+
81
+ # Easter
82
+ easter_date = easter(2026)
83
+ ```
84
+
85
+ ## Development
86
+
87
+ ### Prerequisites
88
+
89
+ - Python 3.10+
90
+ - Rust toolchain
91
+ - [uv](https://github.com/astral-sh/uv) (recommended) or pip
92
+
93
+ ### Setup
94
+
95
+ ```bash
96
+ git clone https://github.com/wakita181009/dateutil-rs.git
97
+ cd dateutil-rs
98
+ uv sync --extra dev
99
+ ```
100
+
101
+ ### Building
102
+
103
+ ```bash
104
+ # Build the native extension
105
+ maturin develop --release
106
+
107
+ # Development build (faster compilation)
108
+ maturin develop -F python
109
+ ```
110
+
111
+ ### Running Tests
112
+
113
+ ```bash
114
+ # Run the test suite
115
+ uv run pytest tests/ -x -q
116
+
117
+ # Run with coverage
118
+ uv run pytest tests/ --cov=dateutil_rs
119
+
120
+ # Run Rust tests
121
+ cargo test -p dateutil-core
122
+ cargo test --workspace
123
+ ```
124
+
125
+ ### Linting
126
+
127
+ ```bash
128
+ uv run ruff check tests/ python/
129
+ uv run ruff format --check tests/ python/
130
+ uv run mypy python/
131
+ cargo clippy --workspace
132
+ ```
133
+
134
+ ### Benchmarks
135
+
136
+ Benchmarks compare the original `python-dateutil` (PyPI) and the Rust extension (`dateutil_rs`) using pytest-benchmark.
137
+
138
+ #### Summary (vs python-dateutil)
139
+
140
+ | Module | Speedup |
141
+ |--------|---------|
142
+ | Parser (parse) | **19.5x-36.0x** |
143
+ | Parser (isoparse) | **13.0x-38.4x** |
144
+ | RRule | **5.9x-63.7x** |
145
+ | Timezone | **1.0x-896.7x** ¹ |
146
+ | RelativeDelta | **2.0x-28.1x** |
147
+ | Easter | **5.0x-7.3x** |
148
+
149
+ > Measured on Apple Silicon (M-series), Python 3.13, release build. Full results: [benchmarks/RESULTS.md](benchmarks/RESULTS.md)
150
+
151
+ ```bash
152
+ # Install the original python-dateutil for comparison
153
+ uv pip install python-dateutil
154
+
155
+ # Run benchmarks
156
+ make bench
157
+
158
+ # Run and save results as JSON
159
+ make bench-save
160
+ ```
161
+
162
+ ## Project Structure
163
+
164
+ ```
165
+ dateutil-rs/
166
+ ├── Cargo.toml # Workspace root
167
+ ├── pyproject.toml # Python project config (maturin)
168
+ ├── crates/
169
+ │ ├── dateutil-core/ # Pure Rust optimized core (crates.io)
170
+ │ │ └── src/
171
+ │ │ ├── lib.rs # Crate root, public API
172
+ │ │ ├── common.rs # Weekday (MO-SU with N-th occurrence)
173
+ │ │ ├── easter.rs # Easter date calculations
174
+ │ │ ├── error.rs # Shared error types
175
+ │ │ ├── relativedelta.rs
176
+ │ │ ├── parser.rs # parse() entry point
177
+ │ │ ├── parser/ # tokenizer, parserinfo, isoparser
178
+ │ │ ├── rrule.rs # RRule entry point
179
+ │ │ ├── rrule/ # iter, parse (rrulestr), set
180
+ │ │ └── tz/ # tzutc, tzoffset, tzfile, tzlocal
181
+ │ └── dateutil-py/ # PyO3 binding layer → PyPI package
182
+ │ └── src/
183
+ │ ├── lib.rs # Module registration
184
+ │ ├── py.rs # Binding root + #[pymodule]
185
+ │ └── py/ # Per-module bindings (common, conv, easter, parser, relativedelta, rrule, tz)
186
+ ├── python/dateutil_rs/ # Python package (maturin mixed layout)
187
+ │ ├── __init__.py # Re-exports from Rust native module
188
+ │ ├── _native.pyi # Type stubs for native module
189
+ │ ├── py.typed # PEP 561 marker
190
+ │ └── parser.py # parserinfo (Python subclass support)
191
+ ├── tests/ # Python test suite
192
+ ├── benchmarks/ # pytest-benchmark comparisons
193
+ ├── .github/workflows/ # CI (ci.yml, publish.yml)
194
+ ├── Makefile
195
+ └── LICENSE
196
+ ```
197
+
198
+ ### Crate Roles
199
+
200
+ | Crate | Purpose | PyO3 | Publish To |
201
+ |-------|---------|------|------------|
202
+ | `dateutil-core` | Pure Rust optimized core | No | crates.io |
203
+ | `dateutil-py` | PyO3 binding layer | Yes | PyPI (`python-dateutil-rs`) |
204
+
205
+ ## Implementation Status
206
+
207
+ | Module | Status | Notes |
208
+ |--------|:------:|-------|
209
+ | common (Weekday) | ✅ | MO-SU constants with N-th occurrence |
210
+ | easter | ✅ | 5.0x-7.3x faster, 3 calendar methods |
211
+ | relativedelta | ✅ | 2.0x-28.1x faster |
212
+ | parser (parse) | ✅ | 19.5x-36.0x faster, zero-copy tokenizer, PHF lookups |
213
+ | parser (isoparse) | ✅ | 13.0x-38.4x faster |
214
+ | parser (parserinfo) | ✅ | Customizable via Python subclass |
215
+ | rrule / rruleset | ✅ | 5.9x-63.7x faster, bitflag filters, buffer reuse |
216
+ | rrulestr | ✅ | RFC 5545 string parsing |
217
+ | tz (tzutc, tzoffset, tzfile, tzlocal) | ✅ | 1.0x-896.7x faster |
218
+ | tz utilities (gettz, datetime_exists, etc.) | ✅ | gettz with caching |
219
+
220
+ ## Roadmap
221
+
222
+ 1. ~~Python-only phase — Pure Python port with full test coverage~~ ✅
223
+ 2. ~~Rust core + PyO3 bindings — easter, relativedelta, parser, weekday~~ ✅
224
+ 3. ~~Rust rrule — Rewrite recurrence rules in Rust~~ ✅
225
+ 4. ~~Rust tz — Rewrite timezone support in Rust~~ ✅
226
+ 5. ~~Optimized core — zero-copy parser, buffer-reusing rrule, consolidated architecture~~ ✅
227
+ 6. **Release** — Publish dateutil-core to crates.io and python-dateutil-rs 1.0 to PyPI
228
+
229
+ ## License
230
+
231
+ [MIT](LICENSE)
232
+
@@ -0,0 +1,209 @@
1
+ # python-dateutil-rs
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/python-dateutil-rs.svg?style=flat-square)](https://pypi.org/project/python-dateutil-rs/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/python-dateutil-rs.svg?style=flat-square)](https://pypi.org/project/python-dateutil-rs/)
5
+ [![License](https://img.shields.io/pypi/l/python-dateutil-rs.svg?style=flat-square)](https://pypi.org/project/python-dateutil-rs/)
6
+ [![CI](https://github.com/wakita181009/dateutil-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/wakita181009/dateutil-rs/actions/workflows/ci.yml)
7
+ [![Coverage](https://codecov.io/gh/wakita181009/dateutil-rs/branch/main/graph/badge.svg)](https://codecov.io/gh/wakita181009/dateutil-rs)
8
+
9
+ A high-performance Rust-backed port of [python-dateutil](https://github.com/dateutil/dateutil) (v2.9.0).
10
+
11
+ > **Status:** All core modules (easter, relativedelta, parser, rrule, tz) are rewritten in Rust via PyO3/maturin. The optimized `dateutil-core` + `dateutil-py` architecture delivers **2x-897x** speedups over python-dateutil.
12
+
13
+ ## Features
14
+
15
+ - **Drop-in replacement** for `python-dateutil` — same API, same behavior
16
+ - **Rust-accelerated:** easter, relativedelta, parser (`parse` / `isoparse`), rrule, tz, weekday
17
+ - **Optimized core:** zero-copy parser, PHF lookup tables, bitflag filters, buffer-reusing rrule
18
+ - **Comprehensive test suite** inherited from the original project
19
+ - **Benchmark infrastructure** for side-by-side performance comparison
20
+ - **Python 3.10-3.14** supported on Linux and macOS
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install python-dateutil-rs
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ from dateutil_rs import (
32
+ parse, isoparse, relativedelta, rrule, rruleset, rrulestr,
33
+ easter, gettz, tzutc, tzoffset,
34
+ MONTHLY, MO, TU, WE, TH, FR, SA, SU,
35
+ )
36
+
37
+ # Parse date strings (zero-copy tokenizer)
38
+ dt = parse("2026-01-15T10:30:00+09:00")
39
+
40
+ # ISO-8601 strict parsing
41
+ dt = isoparse("2026-01-15T10:30:00")
42
+
43
+ # Relative deltas
44
+ next_month = dt + relativedelta(months=+1)
45
+
46
+ # Recurrence rules (buffer-reusing iterator)
47
+ monthly = rrule(MONTHLY, count=5, dtstart=parse("2026-01-01"))
48
+ dates = monthly.all()
49
+ dates = list(monthly) # also iterable
50
+ first = monthly[0] # indexing
51
+ subset = monthly[1:3] # slicing
52
+ n = monthly.count() # total occurrences
53
+ dt in monthly # membership test
54
+
55
+ # Timezones
56
+ tokyo = gettz("Asia/Tokyo")
57
+ utc = tzutc()
58
+
59
+ # Easter
60
+ easter_date = easter(2026)
61
+ ```
62
+
63
+ ## Development
64
+
65
+ ### Prerequisites
66
+
67
+ - Python 3.10+
68
+ - Rust toolchain
69
+ - [uv](https://github.com/astral-sh/uv) (recommended) or pip
70
+
71
+ ### Setup
72
+
73
+ ```bash
74
+ git clone https://github.com/wakita181009/dateutil-rs.git
75
+ cd dateutil-rs
76
+ uv sync --extra dev
77
+ ```
78
+
79
+ ### Building
80
+
81
+ ```bash
82
+ # Build the native extension
83
+ maturin develop --release
84
+
85
+ # Development build (faster compilation)
86
+ maturin develop -F python
87
+ ```
88
+
89
+ ### Running Tests
90
+
91
+ ```bash
92
+ # Run the test suite
93
+ uv run pytest tests/ -x -q
94
+
95
+ # Run with coverage
96
+ uv run pytest tests/ --cov=dateutil_rs
97
+
98
+ # Run Rust tests
99
+ cargo test -p dateutil-core
100
+ cargo test --workspace
101
+ ```
102
+
103
+ ### Linting
104
+
105
+ ```bash
106
+ uv run ruff check tests/ python/
107
+ uv run ruff format --check tests/ python/
108
+ uv run mypy python/
109
+ cargo clippy --workspace
110
+ ```
111
+
112
+ ### Benchmarks
113
+
114
+ Benchmarks compare the original `python-dateutil` (PyPI) and the Rust extension (`dateutil_rs`) using pytest-benchmark.
115
+
116
+ #### Summary (vs python-dateutil)
117
+
118
+ | Module | Speedup |
119
+ |--------|---------|
120
+ | Parser (parse) | **19.5x-36.0x** |
121
+ | Parser (isoparse) | **13.0x-38.4x** |
122
+ | RRule | **5.9x-63.7x** |
123
+ | Timezone | **1.0x-896.7x** ¹ |
124
+ | RelativeDelta | **2.0x-28.1x** |
125
+ | Easter | **5.0x-7.3x** |
126
+
127
+ > Measured on Apple Silicon (M-series), Python 3.13, release build. Full results: [benchmarks/RESULTS.md](benchmarks/RESULTS.md)
128
+
129
+ ```bash
130
+ # Install the original python-dateutil for comparison
131
+ uv pip install python-dateutil
132
+
133
+ # Run benchmarks
134
+ make bench
135
+
136
+ # Run and save results as JSON
137
+ make bench-save
138
+ ```
139
+
140
+ ## Project Structure
141
+
142
+ ```
143
+ dateutil-rs/
144
+ ├── Cargo.toml # Workspace root
145
+ ├── pyproject.toml # Python project config (maturin)
146
+ ├── crates/
147
+ │ ├── dateutil-core/ # Pure Rust optimized core (crates.io)
148
+ │ │ └── src/
149
+ │ │ ├── lib.rs # Crate root, public API
150
+ │ │ ├── common.rs # Weekday (MO-SU with N-th occurrence)
151
+ │ │ ├── easter.rs # Easter date calculations
152
+ │ │ ├── error.rs # Shared error types
153
+ │ │ ├── relativedelta.rs
154
+ │ │ ├── parser.rs # parse() entry point
155
+ │ │ ├── parser/ # tokenizer, parserinfo, isoparser
156
+ │ │ ├── rrule.rs # RRule entry point
157
+ │ │ ├── rrule/ # iter, parse (rrulestr), set
158
+ │ │ └── tz/ # tzutc, tzoffset, tzfile, tzlocal
159
+ │ └── dateutil-py/ # PyO3 binding layer → PyPI package
160
+ │ └── src/
161
+ │ ├── lib.rs # Module registration
162
+ │ ├── py.rs # Binding root + #[pymodule]
163
+ │ └── py/ # Per-module bindings (common, conv, easter, parser, relativedelta, rrule, tz)
164
+ ├── python/dateutil_rs/ # Python package (maturin mixed layout)
165
+ │ ├── __init__.py # Re-exports from Rust native module
166
+ │ ├── _native.pyi # Type stubs for native module
167
+ │ ├── py.typed # PEP 561 marker
168
+ │ └── parser.py # parserinfo (Python subclass support)
169
+ ├── tests/ # Python test suite
170
+ ├── benchmarks/ # pytest-benchmark comparisons
171
+ ├── .github/workflows/ # CI (ci.yml, publish.yml)
172
+ ├── Makefile
173
+ └── LICENSE
174
+ ```
175
+
176
+ ### Crate Roles
177
+
178
+ | Crate | Purpose | PyO3 | Publish To |
179
+ |-------|---------|------|------------|
180
+ | `dateutil-core` | Pure Rust optimized core | No | crates.io |
181
+ | `dateutil-py` | PyO3 binding layer | Yes | PyPI (`python-dateutil-rs`) |
182
+
183
+ ## Implementation Status
184
+
185
+ | Module | Status | Notes |
186
+ |--------|:------:|-------|
187
+ | common (Weekday) | ✅ | MO-SU constants with N-th occurrence |
188
+ | easter | ✅ | 5.0x-7.3x faster, 3 calendar methods |
189
+ | relativedelta | ✅ | 2.0x-28.1x faster |
190
+ | parser (parse) | ✅ | 19.5x-36.0x faster, zero-copy tokenizer, PHF lookups |
191
+ | parser (isoparse) | ✅ | 13.0x-38.4x faster |
192
+ | parser (parserinfo) | ✅ | Customizable via Python subclass |
193
+ | rrule / rruleset | ✅ | 5.9x-63.7x faster, bitflag filters, buffer reuse |
194
+ | rrulestr | ✅ | RFC 5545 string parsing |
195
+ | tz (tzutc, tzoffset, tzfile, tzlocal) | ✅ | 1.0x-896.7x faster |
196
+ | tz utilities (gettz, datetime_exists, etc.) | ✅ | gettz with caching |
197
+
198
+ ## Roadmap
199
+
200
+ 1. ~~Python-only phase — Pure Python port with full test coverage~~ ✅
201
+ 2. ~~Rust core + PyO3 bindings — easter, relativedelta, parser, weekday~~ ✅
202
+ 3. ~~Rust rrule — Rewrite recurrence rules in Rust~~ ✅
203
+ 4. ~~Rust tz — Rewrite timezone support in Rust~~ ✅
204
+ 5. ~~Optimized core — zero-copy parser, buffer-reusing rrule, consolidated architecture~~ ✅
205
+ 6. **Release** — Publish dateutil-core to crates.io and python-dateutil-rs 1.0 to PyPI
206
+
207
+ ## License
208
+
209
+ [MIT](LICENSE)
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "dateutil-core"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  edition = "2021"
5
5
  license = "MIT"
6
6
  description = "Performance-optimized date utility library for Rust"
@@ -5,7 +5,8 @@ use std::fmt;
5
5
  ///
6
6
  /// The `weekday` field is 0-based: 0=Monday, 1=Tuesday, ..., 6=Sunday.
7
7
  /// The `n` field indicates the N-th occurrence (e.g., 2nd Tuesday = TU(+2)).
8
- /// When `n` is `None` or `Some(0)`, only the day name is displayed.
8
+ /// When `n` is `None`, only the day name is displayed.
9
+ /// `n = Some(0)` is rejected at construction time (`Weekday::new`).
9
10
  #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10
11
  pub struct Weekday {
11
12
  weekday: u8,
@@ -22,6 +23,9 @@ impl Weekday {
22
23
  if weekday > 6 {
23
24
  return Err(WeekdayError::InvalidWeekday(weekday));
24
25
  }
26
+ if n == Some(0) {
27
+ return Err(WeekdayError::InvalidN);
28
+ }
25
29
  Ok(Self { weekday, n })
26
30
  }
27
31
 
@@ -36,19 +40,28 @@ impl Weekday {
36
40
  }
37
41
 
38
42
  /// Create a new Weekday with the same day but different `n`.
43
+ /// `Some(0)` is normalized to `None` (n=0 is semantically "any occurrence").
39
44
  pub fn with_n(&self, n: Option<i32>) -> Self {
40
45
  Self {
41
46
  weekday: self.weekday,
42
- n,
47
+ n: if n == Some(0) { None } else { n },
43
48
  }
44
49
  }
45
50
  }
46
51
 
52
+ impl TryFrom<u8> for Weekday {
53
+ type Error = WeekdayError;
54
+
55
+ fn try_from(value: u8) -> Result<Self, Self::Error> {
56
+ Self::new(value, None)
57
+ }
58
+ }
59
+
47
60
  impl fmt::Display for Weekday {
48
61
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49
62
  let name = DAY_NAMES[self.weekday as usize];
50
63
  match self.n {
51
- None | Some(0) => write!(f, "{name}"),
64
+ None => write!(f, "{name}"),
52
65
  Some(n) => write!(f, "{name}({n:+})"),
53
66
  }
54
67
  }
@@ -124,9 +137,11 @@ mod tests {
124
137
  }
125
138
 
126
139
  #[test]
127
- fn test_weekday_display_n_zero() {
128
- let wd = Weekday::new(0, Some(0)).unwrap();
129
- assert_eq!(wd.to_string(), "MO");
140
+ fn test_weekday_n_zero_rejected() {
141
+ assert!(matches!(
142
+ Weekday::new(0, Some(0)),
143
+ Err(WeekdayError::InvalidN)
144
+ ));
130
145
  }
131
146
 
132
147
  #[test]
@@ -174,30 +189,6 @@ mod tests {
174
189
  assert_eq!(wd_neg.to_string(), "SU(-100)");
175
190
  }
176
191
 
177
- #[test]
178
- fn test_weekday_negative_one_n() {
179
- // Last occurrence (e.g., last Friday of month)
180
- let wd = FR.with_n(Some(-1));
181
- assert_eq!(wd.n(), Some(-1));
182
- assert_eq!(wd.to_string(), "FR(-1)");
183
- }
184
-
185
- #[test]
186
- fn test_weekday_clone_copy() {
187
- let wd = MO.with_n(Some(2));
188
- let cloned = wd;
189
- assert_eq!(wd, cloned); // Copy semantics — both usable
190
- }
191
-
192
- #[test]
193
- fn test_weekday_boundary_values() {
194
- // Weekday 0 (Monday) and 6 (Sunday) are boundaries
195
- let mon = Weekday::new(0, Some(1)).unwrap();
196
- let sun = Weekday::new(6, Some(-1)).unwrap();
197
- assert_eq!(mon.weekday(), 0);
198
- assert_eq!(sun.weekday(), 6);
199
- }
200
-
201
192
  #[test]
202
193
  fn test_weekday_all_invalid() {
203
194
  for i in 7..=255 {
@@ -212,15 +203,6 @@ mod tests {
212
203
  assert_ne!(a, b);
213
204
  }
214
205
 
215
- #[test]
216
- fn test_weekday_eq_none_vs_zero() {
217
- let a = MO.with_n(None);
218
- let b = MO.with_n(Some(0));
219
- // Display is the same but PartialEq differs (n field differs)
220
- assert_eq!(a.to_string(), b.to_string());
221
- assert_ne!(a, b);
222
- }
223
-
224
206
  #[test]
225
207
  fn test_weekday_hash_with_n() {
226
208
  use std::collections::HashSet;
@@ -268,19 +250,24 @@ mod tests {
268
250
  }
269
251
 
270
252
  #[test]
271
- fn test_weekday_equality_ignores_n_for_same_display() {
272
- let a = MO.with_n(Some(1));
273
- let b = MO.with_n(Some(-1));
274
- assert_ne!(a, b);
275
- assert_eq!(a.weekday(), b.weekday());
276
- }
277
-
278
- #[test]
279
- fn test_weekday_hash_set_with_n_none_vs_some0() {
253
+ fn test_weekday_hash_set_none() {
280
254
  use std::collections::HashSet;
281
255
  let mut set = HashSet::new();
282
256
  set.insert(MO.with_n(None));
283
- set.insert(MO.with_n(Some(0)));
257
+ set.insert(MO.with_n(Some(1)));
284
258
  assert_eq!(set.len(), 2);
285
259
  }
260
+
261
+ #[test]
262
+ fn test_weekday_try_from_valid() {
263
+ let wd: Weekday = 3u8.try_into().unwrap();
264
+ assert_eq!(wd.weekday(), 3);
265
+ assert_eq!(wd.n(), None);
266
+ }
267
+
268
+ #[test]
269
+ fn test_weekday_try_from_invalid() {
270
+ let result: Result<Weekday, _> = 7u8.try_into();
271
+ assert!(result.is_err());
272
+ }
286
273
  }
@@ -71,30 +71,6 @@ mod tests {
71
71
  use super::*;
72
72
  use chrono::Datelike;
73
73
 
74
- #[test]
75
- fn test_western_2024() {
76
- assert_eq!(
77
- easter(2024, EasterMethod::Western).unwrap(),
78
- NaiveDate::from_ymd_opt(2024, 3, 31).unwrap()
79
- );
80
- }
81
-
82
- #[test]
83
- fn test_orthodox_2024() {
84
- assert_eq!(
85
- easter(2024, EasterMethod::Orthodox).unwrap(),
86
- NaiveDate::from_ymd_opt(2024, 5, 5).unwrap()
87
- );
88
- }
89
-
90
- #[test]
91
- fn test_julian_326() {
92
- assert_eq!(
93
- easter(326, EasterMethod::Julian).unwrap(),
94
- NaiveDate::from_ymd_opt(326, 4, 3).unwrap()
95
- );
96
- }
97
-
98
74
  #[test]
99
75
  fn test_invalid_method_from_i32() {
100
76
  assert!(matches!(
@@ -107,18 +83,6 @@ mod tests {
107
83
  ));
108
84
  }
109
85
 
110
- #[test]
111
- fn test_invalid_year() {
112
- assert!(matches!(
113
- easter(0, EasterMethod::Western),
114
- Err(EasterError::InvalidYear(0))
115
- ));
116
- assert!(matches!(
117
- easter(-1, EasterMethod::Western),
118
- Err(EasterError::InvalidYear(-1))
119
- ));
120
- }
121
-
122
86
  #[test]
123
87
  fn test_western_range_1990_2050() {
124
88
  let expected: Vec<(i32, u32, u32)> = vec![
@@ -347,14 +311,6 @@ mod tests {
347
311
  );
348
312
  }
349
313
 
350
- #[test]
351
- fn test_orthodox_boundary_exact_1600() {
352
- let d1600 = easter(1600, EasterMethod::Orthodox).unwrap();
353
- let d1601 = easter(1601, EasterMethod::Orthodox).unwrap();
354
- assert!((3..=5).contains(&d1600.month()));
355
- assert!((3..=5).contains(&d1601.month()));
356
- }
357
-
358
314
  #[test]
359
315
  fn test_easter_always_sunday_western_wide_range() {
360
316
  // Western (Gregorian) Easter is always Sunday across a wide year range