python-dateutil-rs 0.1.1__tar.gz → 0.1.2__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.
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/Cargo.lock +11 -4
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/PKG-INFO +2 -2
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/README.md +1 -1
- python_dateutil_rs-0.1.2/crates/dateutil-core/Cargo.toml +30 -0
- python_dateutil_rs-0.1.2/crates/dateutil-core/README.md +209 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/benches/benchmarks.rs +8 -8
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/rrule/parse.rs +5 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/tz.rs +71 -18
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-py/Cargo.toml +2 -2
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-py/src/py/common.rs +31 -3
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-py/src/py/conv.rs +20 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-py/src/py/easter.rs +1 -1
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-py/src/py/parser.rs +2 -2
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-py/src/py/relativedelta.rs +3 -3
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-py/src/py/rrule.rs +179 -29
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-py/src/py/tz.rs +51 -11
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/pyproject.toml +1 -1
- python_dateutil_rs-0.1.1/crates/dateutil-core/Cargo.toml +0 -25
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/Cargo.toml +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/LICENSE +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/CLAUDE.md +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/common.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/easter.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/error.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/lib.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/parser/isoparser.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/parser/parserinfo.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/parser/tokenizer.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/parser.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/relativedelta.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/rrule/iter.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/rrule/set.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/rrule.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/tz/file.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/tz/local.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/tz/offset.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/tz/utc.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-py/src/lib.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-py/src/py.rs +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/python/dateutil_rs/__init__.py +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/python/dateutil_rs/_native.pyi +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/python/dateutil_rs/easter.py +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/python/dateutil_rs/parser.py +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/python/dateutil_rs/py.typed +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/python/dateutil_rs/relativedelta.py +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/python/dateutil_rs/rrule.py +0 -0
- {python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/python/dateutil_rs/tz.py +0 -0
|
@@ -219,24 +219,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
|
219
219
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
|
220
220
|
|
|
221
221
|
[[package]]
|
|
222
|
-
name = "dateutil
|
|
223
|
-
version = "0.1.
|
|
222
|
+
name = "dateutil"
|
|
223
|
+
version = "0.1.2"
|
|
224
224
|
dependencies = [
|
|
225
225
|
"bitflags",
|
|
226
226
|
"chrono",
|
|
227
227
|
"criterion",
|
|
228
228
|
"iana-time-zone",
|
|
229
229
|
"phf",
|
|
230
|
+
"rustc-hash",
|
|
230
231
|
"smallvec",
|
|
231
232
|
"thiserror",
|
|
232
233
|
]
|
|
233
234
|
|
|
234
235
|
[[package]]
|
|
235
236
|
name = "dateutil-py"
|
|
236
|
-
version = "0.1.
|
|
237
|
+
version = "0.1.2"
|
|
237
238
|
dependencies = [
|
|
238
239
|
"chrono",
|
|
239
|
-
"dateutil
|
|
240
|
+
"dateutil",
|
|
240
241
|
"pyo3",
|
|
241
242
|
]
|
|
242
243
|
|
|
@@ -576,6 +577,12 @@ version = "0.8.10"
|
|
|
576
577
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
577
578
|
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
|
578
579
|
|
|
580
|
+
[[package]]
|
|
581
|
+
name = "rustc-hash"
|
|
582
|
+
version = "2.1.2"
|
|
583
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
584
|
+
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
|
585
|
+
|
|
579
586
|
[[package]]
|
|
580
587
|
name = "rustversion"
|
|
581
588
|
version = "1.0.22"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-dateutil-rs
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Classifier: Programming Language :: Python :: 3.10
|
|
5
5
|
Classifier: Programming Language :: Python :: 3.11
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -142,7 +142,7 @@ Benchmarks compare the original `python-dateutil` (PyPI) and the Rust extension
|
|
|
142
142
|
| Parser (parse) | **19.5x-36.0x** |
|
|
143
143
|
| Parser (isoparse) | **13.0x-38.4x** |
|
|
144
144
|
| RRule | **5.9x-63.7x** |
|
|
145
|
-
| Timezone | **1.0x-896.7x**
|
|
145
|
+
| Timezone | **1.0x-896.7x** |
|
|
146
146
|
| RelativeDelta | **2.0x-28.1x** |
|
|
147
147
|
| Easter | **5.0x-7.3x** |
|
|
148
148
|
|
|
@@ -120,7 +120,7 @@ Benchmarks compare the original `python-dateutil` (PyPI) and the Rust extension
|
|
|
120
120
|
| Parser (parse) | **19.5x-36.0x** |
|
|
121
121
|
| Parser (isoparse) | **13.0x-38.4x** |
|
|
122
122
|
| RRule | **5.9x-63.7x** |
|
|
123
|
-
| Timezone | **1.0x-896.7x**
|
|
123
|
+
| Timezone | **1.0x-896.7x** |
|
|
124
124
|
| RelativeDelta | **2.0x-28.1x** |
|
|
125
125
|
| Easter | **5.0x-7.3x** |
|
|
126
126
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "dateutil"
|
|
3
|
+
version = "0.1.2"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
description = "Fast date utility library — parser, relativedelta, rrule, timezone (Rust reimplementation of python-dateutil)"
|
|
7
|
+
repository = "https://github.com/wakita181009/dateutil-rs"
|
|
8
|
+
keywords = ["date", "datetime", "parser", "rrule", "timezone"]
|
|
9
|
+
categories = ["date-and-time", "parser-implementations"]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
|
|
12
|
+
[lib]
|
|
13
|
+
name = "dateutil"
|
|
14
|
+
crate-type = ["rlib"]
|
|
15
|
+
|
|
16
|
+
[dependencies]
|
|
17
|
+
bitflags = "2"
|
|
18
|
+
chrono = "0.4"
|
|
19
|
+
phf = { version = "0.13", features = ["macros"] }
|
|
20
|
+
rustc-hash = "2"
|
|
21
|
+
smallvec = "1.15"
|
|
22
|
+
thiserror = "2"
|
|
23
|
+
iana-time-zone = "0.1"
|
|
24
|
+
|
|
25
|
+
[dev-dependencies]
|
|
26
|
+
criterion = { version = "0.8", features = ["html_reports"] }
|
|
27
|
+
|
|
28
|
+
[[bench]]
|
|
29
|
+
name = "benchmarks"
|
|
30
|
+
harness = false
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# python-dateutil-rs
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/python-dateutil-rs/)
|
|
4
|
+
[](https://pypi.org/project/python-dateutil-rs/)
|
|
5
|
+
[](https://pypi.org/project/python-dateutil-rs/)
|
|
6
|
+
[](https://github.com/wakita181009/dateutil-rs/actions/workflows/ci.yml)
|
|
7
|
+
[](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)
|
{python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/benches/benchmarks.rs
RENAMED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
use criterion::{criterion_group, criterion_main, Criterion};
|
|
2
2
|
use std::hint::black_box;
|
|
3
3
|
use chrono::NaiveDate;
|
|
4
|
-
use
|
|
5
|
-
use
|
|
6
|
-
use
|
|
7
|
-
use
|
|
8
|
-
use
|
|
9
|
-
use
|
|
10
|
-
use
|
|
11
|
-
use
|
|
4
|
+
use dateutil::common::Weekday;
|
|
5
|
+
use dateutil::easter::{easter, EasterMethod};
|
|
6
|
+
use dateutil::parser;
|
|
7
|
+
use dateutil::parser::tokenizer;
|
|
8
|
+
use dateutil::relativedelta::RelativeDelta;
|
|
9
|
+
use dateutil::rrule::{Frequency, Recurrence, RRuleBuilder};
|
|
10
|
+
use dateutil::rrule::parse::rrulestr;
|
|
11
|
+
use dateutil::rrule::set::RRuleSet;
|
|
12
12
|
|
|
13
13
|
fn bench_tokenizer(c: &mut Criterion) {
|
|
14
14
|
c.bench_function("tokenize_simple_date", |b| {
|
{python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/rrule/parse.rs
RENAMED
|
@@ -115,6 +115,11 @@ pub fn rrulestr(
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
} else if prop.eq_ignore_ascii_case("DTSTART") {
|
|
118
|
+
if value.contains(',') {
|
|
119
|
+
return Err(RRuleError::ValueError(
|
|
120
|
+
"DTSTART must be a single date-time value".into(),
|
|
121
|
+
));
|
|
122
|
+
}
|
|
118
123
|
if let Some(dt) = parse_rfc_datetime(value) {
|
|
119
124
|
dtstart = Some(dt);
|
|
120
125
|
}
|
|
@@ -13,13 +13,70 @@ pub use offset::TzOffset;
|
|
|
13
13
|
pub use file::{TzFile, TzFileData};
|
|
14
14
|
pub use local::TzLocal;
|
|
15
15
|
|
|
16
|
-
use std::collections::HashMap;
|
|
17
16
|
use std::sync::{LazyLock, RwLock};
|
|
18
17
|
|
|
18
|
+
use rustc_hash::FxHashMap;
|
|
19
|
+
|
|
19
20
|
use chrono::{NaiveDateTime, TimeDelta};
|
|
20
21
|
|
|
21
22
|
use crate::error::TzError;
|
|
22
23
|
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// TzOps — shared trait for all timezone types
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/// Core timezone operations shared by all timezone types.
|
|
29
|
+
///
|
|
30
|
+
/// Implemented by `TzUtc`, `TzOffset`, `TzFile`, `TzLocal`, and `TimeZone`.
|
|
31
|
+
/// Helper functions like `datetime_exists` and `resolve_imaginary` are generic
|
|
32
|
+
/// over this trait, so callers can use any timezone type without constructing
|
|
33
|
+
/// the `TimeZone` enum (avoiding clones in the PyO3 binding layer).
|
|
34
|
+
pub trait TzOps {
|
|
35
|
+
/// UTC offset in seconds for a wall-clock datetime.
|
|
36
|
+
/// `fold` disambiguates repeated wall times (PEP 495).
|
|
37
|
+
fn utcoffset(&self, dt: NaiveDateTime, fold: bool) -> i32;
|
|
38
|
+
/// DST offset component in seconds.
|
|
39
|
+
fn dst(&self, dt: NaiveDateTime, fold: bool) -> i32;
|
|
40
|
+
/// Timezone abbreviation string.
|
|
41
|
+
fn tzname(&self, dt: NaiveDateTime, fold: bool) -> &str;
|
|
42
|
+
/// Whether the given wall time is ambiguous (falls in a DST overlap).
|
|
43
|
+
fn is_ambiguous(&self, dt: NaiveDateTime) -> bool;
|
|
44
|
+
/// Convert a UTC datetime to wall time.
|
|
45
|
+
fn fromutc(&self, dt: NaiveDateTime) -> NaiveDateTime;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
impl TzOps for TzUtc {
|
|
49
|
+
#[inline] fn utcoffset(&self, dt: NaiveDateTime, fold: bool) -> i32 { self.utcoffset(dt, fold) }
|
|
50
|
+
#[inline] fn dst(&self, dt: NaiveDateTime, fold: bool) -> i32 { self.dst(dt, fold) }
|
|
51
|
+
#[inline] fn tzname(&self, dt: NaiveDateTime, fold: bool) -> &str { self.tzname(dt, fold) }
|
|
52
|
+
#[inline] fn is_ambiguous(&self, dt: NaiveDateTime) -> bool { self.is_ambiguous(dt) }
|
|
53
|
+
#[inline] fn fromutc(&self, dt: NaiveDateTime) -> NaiveDateTime { self.fromutc(dt) }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
impl TzOps for TzOffset {
|
|
57
|
+
#[inline] fn utcoffset(&self, dt: NaiveDateTime, fold: bool) -> i32 { self.utcoffset(dt, fold) }
|
|
58
|
+
#[inline] fn dst(&self, dt: NaiveDateTime, fold: bool) -> i32 { self.dst(dt, fold) }
|
|
59
|
+
#[inline] fn tzname(&self, dt: NaiveDateTime, fold: bool) -> &str { self.tzname(dt, fold) }
|
|
60
|
+
#[inline] fn is_ambiguous(&self, dt: NaiveDateTime) -> bool { self.is_ambiguous(dt) }
|
|
61
|
+
#[inline] fn fromutc(&self, dt: NaiveDateTime) -> NaiveDateTime { self.fromutc(dt) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
impl TzOps for TzFile {
|
|
65
|
+
#[inline] fn utcoffset(&self, dt: NaiveDateTime, fold: bool) -> i32 { self.utcoffset(dt, fold) }
|
|
66
|
+
#[inline] fn dst(&self, dt: NaiveDateTime, fold: bool) -> i32 { self.dst(dt, fold) }
|
|
67
|
+
#[inline] fn tzname(&self, dt: NaiveDateTime, fold: bool) -> &str { self.tzname(dt, fold) }
|
|
68
|
+
#[inline] fn is_ambiguous(&self, dt: NaiveDateTime) -> bool { self.is_ambiguous(dt) }
|
|
69
|
+
#[inline] fn fromutc(&self, dt: NaiveDateTime) -> NaiveDateTime { self.fromutc(dt) }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
impl TzOps for TzLocal {
|
|
73
|
+
#[inline] fn utcoffset(&self, dt: NaiveDateTime, fold: bool) -> i32 { self.utcoffset(dt, fold) }
|
|
74
|
+
#[inline] fn dst(&self, dt: NaiveDateTime, fold: bool) -> i32 { self.dst(dt, fold) }
|
|
75
|
+
#[inline] fn tzname(&self, dt: NaiveDateTime, fold: bool) -> &str { self.tzname(dt, fold) }
|
|
76
|
+
#[inline] fn is_ambiguous(&self, dt: NaiveDateTime) -> bool { self.is_ambiguous(dt) }
|
|
77
|
+
#[inline] fn fromutc(&self, dt: NaiveDateTime) -> NaiveDateTime { self.fromutc(dt) }
|
|
78
|
+
}
|
|
79
|
+
|
|
23
80
|
// ---------------------------------------------------------------------------
|
|
24
81
|
// TimeZone — enum dispatch for all timezone types
|
|
25
82
|
// ---------------------------------------------------------------------------
|
|
@@ -37,11 +94,9 @@ pub enum TimeZone {
|
|
|
37
94
|
Local(TzLocal),
|
|
38
95
|
}
|
|
39
96
|
|
|
40
|
-
impl TimeZone {
|
|
41
|
-
/// UTC offset in seconds for a wall-clock datetime.
|
|
42
|
-
/// `fold` disambiguates repeated wall times (PEP 495).
|
|
97
|
+
impl TzOps for TimeZone {
|
|
43
98
|
#[inline]
|
|
44
|
-
|
|
99
|
+
fn utcoffset(&self, dt: NaiveDateTime, fold: bool) -> i32 {
|
|
45
100
|
match self {
|
|
46
101
|
TimeZone::Utc(tz) => tz.utcoffset(dt, fold),
|
|
47
102
|
TimeZone::Offset(tz) => tz.utcoffset(dt, fold),
|
|
@@ -50,9 +105,8 @@ impl TimeZone {
|
|
|
50
105
|
}
|
|
51
106
|
}
|
|
52
107
|
|
|
53
|
-
/// DST offset component in seconds.
|
|
54
108
|
#[inline]
|
|
55
|
-
|
|
109
|
+
fn dst(&self, dt: NaiveDateTime, fold: bool) -> i32 {
|
|
56
110
|
match self {
|
|
57
111
|
TimeZone::Utc(tz) => tz.dst(dt, fold),
|
|
58
112
|
TimeZone::Offset(tz) => tz.dst(dt, fold),
|
|
@@ -61,9 +115,8 @@ impl TimeZone {
|
|
|
61
115
|
}
|
|
62
116
|
}
|
|
63
117
|
|
|
64
|
-
/// Timezone abbreviation string.
|
|
65
118
|
#[inline]
|
|
66
|
-
|
|
119
|
+
fn tzname(&self, dt: NaiveDateTime, fold: bool) -> &str {
|
|
67
120
|
match self {
|
|
68
121
|
TimeZone::Utc(tz) => tz.tzname(dt, fold),
|
|
69
122
|
TimeZone::Offset(tz) => tz.tzname(dt, fold),
|
|
@@ -72,9 +125,8 @@ impl TimeZone {
|
|
|
72
125
|
}
|
|
73
126
|
}
|
|
74
127
|
|
|
75
|
-
/// Whether the given wall time is ambiguous (falls in a DST overlap).
|
|
76
128
|
#[inline]
|
|
77
|
-
|
|
129
|
+
fn is_ambiguous(&self, dt: NaiveDateTime) -> bool {
|
|
78
130
|
match self {
|
|
79
131
|
TimeZone::Utc(tz) => tz.is_ambiguous(dt),
|
|
80
132
|
TimeZone::Offset(tz) => tz.is_ambiguous(dt),
|
|
@@ -83,9 +135,8 @@ impl TimeZone {
|
|
|
83
135
|
}
|
|
84
136
|
}
|
|
85
137
|
|
|
86
|
-
/// Convert a UTC datetime to wall time.
|
|
87
138
|
#[inline]
|
|
88
|
-
|
|
139
|
+
fn fromutc(&self, dt: NaiveDateTime) -> NaiveDateTime {
|
|
89
140
|
match self {
|
|
90
141
|
TimeZone::Utc(tz) => tz.fromutc(dt),
|
|
91
142
|
TimeZone::Offset(tz) => tz.fromutc(dt),
|
|
@@ -93,7 +144,9 @@ impl TimeZone {
|
|
|
93
144
|
TimeZone::Local(tz) => tz.fromutc(dt),
|
|
94
145
|
}
|
|
95
146
|
}
|
|
147
|
+
}
|
|
96
148
|
|
|
149
|
+
impl TimeZone {
|
|
97
150
|
/// UTC offset as a `chrono::TimeDelta`.
|
|
98
151
|
#[inline]
|
|
99
152
|
pub fn utcoffset_delta(&self, dt: NaiveDateTime, fold: bool) -> TimeDelta {
|
|
@@ -147,8 +200,8 @@ pub(super) const TZPATHS: &[&str] = &[
|
|
|
147
200
|
const UTC_NAMES: &[&str] = &["UTC", "utc", "GMT", "gmt", "Z", "z"];
|
|
148
201
|
|
|
149
202
|
/// Thread-safe timezone cache.
|
|
150
|
-
static TZ_CACHE: LazyLock<RwLock<
|
|
151
|
-
LazyLock::new(|| RwLock::new(
|
|
203
|
+
static TZ_CACHE: LazyLock<RwLock<FxHashMap<Box<str>, TimeZone>>> =
|
|
204
|
+
LazyLock::new(|| RwLock::new(FxHashMap::default()));
|
|
152
205
|
|
|
153
206
|
/// Look up a timezone by name.
|
|
154
207
|
///
|
|
@@ -228,7 +281,7 @@ fn resolve_tz(name: &str) -> Result<TimeZone, TzError> {
|
|
|
228
281
|
|
|
229
282
|
/// Check if a wall-clock datetime exists in the given timezone.
|
|
230
283
|
/// Returns `false` for times in DST gaps (spring forward).
|
|
231
|
-
pub fn datetime_exists(dt: NaiveDateTime, tz: &
|
|
284
|
+
pub fn datetime_exists(dt: NaiveDateTime, tz: &impl TzOps) -> bool {
|
|
232
285
|
let offset_secs = tz.utcoffset(dt, false) as i64;
|
|
233
286
|
let utc = dt - TimeDelta::seconds(offset_secs);
|
|
234
287
|
let wall = tz.fromutc(utc);
|
|
@@ -237,13 +290,13 @@ pub fn datetime_exists(dt: NaiveDateTime, tz: &TimeZone) -> bool {
|
|
|
237
290
|
|
|
238
291
|
/// Check if a wall-clock datetime is ambiguous in the given timezone.
|
|
239
292
|
/// Returns `true` for times in DST overlaps (fall back).
|
|
240
|
-
pub fn datetime_ambiguous(dt: NaiveDateTime, tz: &
|
|
293
|
+
pub fn datetime_ambiguous(dt: NaiveDateTime, tz: &impl TzOps) -> bool {
|
|
241
294
|
tz.is_ambiguous(dt)
|
|
242
295
|
}
|
|
243
296
|
|
|
244
297
|
/// Resolve an imaginary datetime (in a DST gap) by shifting forward.
|
|
245
298
|
/// If the datetime already exists, returns it unchanged.
|
|
246
|
-
pub fn resolve_imaginary(dt: NaiveDateTime, tz: &
|
|
299
|
+
pub fn resolve_imaginary(dt: NaiveDateTime, tz: &impl TzOps) -> NaiveDateTime {
|
|
247
300
|
if datetime_exists(dt, tz) {
|
|
248
301
|
return dt;
|
|
249
302
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "dateutil-py"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
description = "PyO3 bindings for the dateutil-core crate"
|
|
@@ -10,7 +10,7 @@ name = "dateutil_py"
|
|
|
10
10
|
crate-type = ["rlib"]
|
|
11
11
|
|
|
12
12
|
[dependencies]
|
|
13
|
-
dateutil
|
|
13
|
+
dateutil = { path = "../dateutil-core" }
|
|
14
14
|
chrono = "0.4"
|
|
15
15
|
pyo3 = { version = "0.28", features = ["extension-module", "chrono"], optional = true }
|
|
16
16
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
use
|
|
1
|
+
use dateutil::common;
|
|
2
2
|
use pyo3::prelude::*;
|
|
3
3
|
|
|
4
|
-
/// Python wrapper for
|
|
5
|
-
#[pyclass(name = "weekday", frozen,
|
|
4
|
+
/// Python wrapper for dateutil::common::Weekday.
|
|
5
|
+
#[pyclass(name = "weekday", frozen, from_py_object)]
|
|
6
6
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
7
7
|
pub struct PyWeekday {
|
|
8
8
|
inner: common::Weekday,
|
|
@@ -47,6 +47,34 @@ impl PyWeekday {
|
|
|
47
47
|
self.inner.n()
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
fn __hash__(&self) -> isize {
|
|
51
|
+
use std::hash::{Hash, Hasher};
|
|
52
|
+
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
|
53
|
+
self.inner.hash(&mut hasher);
|
|
54
|
+
hasher.finish() as isize
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn __eq__(&self, _py: Python<'_>, other: &Bound<'_, PyAny>) -> bool {
|
|
58
|
+
// Fast path: other is a PyWeekday
|
|
59
|
+
if let Ok(wd) = other.extract::<PyWeekday>() {
|
|
60
|
+
return self.inner == wd.inner;
|
|
61
|
+
}
|
|
62
|
+
// Duck-type: must have both `weekday` AND `n` attributes
|
|
63
|
+
let Ok(wd_attr) = other.getattr("weekday") else {
|
|
64
|
+
return false;
|
|
65
|
+
};
|
|
66
|
+
let Ok(n_attr) = other.getattr("n") else {
|
|
67
|
+
return false;
|
|
68
|
+
};
|
|
69
|
+
let Ok(wd_val) = wd_attr.extract::<u8>() else {
|
|
70
|
+
return false;
|
|
71
|
+
};
|
|
72
|
+
let Ok(n_val) = n_attr.extract::<Option<i32>>() else {
|
|
73
|
+
return false;
|
|
74
|
+
};
|
|
75
|
+
self.inner.weekday() == wd_val && self.inner.n() == n_val
|
|
76
|
+
}
|
|
77
|
+
|
|
50
78
|
fn __repr__(&self) -> String {
|
|
51
79
|
self.inner.to_string()
|
|
52
80
|
}
|
|
@@ -148,6 +148,26 @@ pub fn ndt_to_py_datetime_with_fold<'py>(
|
|
|
148
148
|
)
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Scalar-or-sequence extraction helpers
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
/// Accept either a single `i32` or a list of `i32`.
|
|
156
|
+
pub fn extract_i32_list(obj: &Bound<'_, PyAny>) -> PyResult<Vec<i32>> {
|
|
157
|
+
if let Ok(v) = obj.extract::<i32>() {
|
|
158
|
+
return Ok(vec![v]);
|
|
159
|
+
}
|
|
160
|
+
obj.extract::<Vec<i32>>()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Accept either a single `u8` or a list of `u8`.
|
|
164
|
+
pub fn extract_u8_list(obj: &Bound<'_, PyAny>) -> PyResult<Vec<u8>> {
|
|
165
|
+
if let Ok(v) = obj.extract::<u8>() {
|
|
166
|
+
return Ok(vec![v]);
|
|
167
|
+
}
|
|
168
|
+
obj.extract::<Vec<u8>>()
|
|
169
|
+
}
|
|
170
|
+
|
|
151
171
|
// ---------------------------------------------------------------------------
|
|
152
172
|
// Timezone / timedelta helpers
|
|
153
173
|
// ---------------------------------------------------------------------------
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
use std::collections::HashMap;
|
|
2
2
|
|
|
3
3
|
use super::conv::{make_py_tz, make_py_utc, ndt_to_py_datetime};
|
|
4
|
-
use
|
|
5
|
-
use
|
|
4
|
+
use dateutil::parser;
|
|
5
|
+
use dateutil::parser::ParserInfo;
|
|
6
6
|
use pyo3::prelude::*;
|
|
7
7
|
use pyo3::types::{PyDict, PyTzInfo, PyType};
|
|
8
8
|
|
{python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-py/src/py/relativedelta.rs
RENAMED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
use super::common::PyWeekday;
|
|
2
2
|
use super::conv;
|
|
3
3
|
use chrono::{Datelike, NaiveDateTime};
|
|
4
|
-
use
|
|
5
|
-
use
|
|
4
|
+
use dateutil::common;
|
|
5
|
+
use dateutil::relativedelta::{RelativeDelta, RelativeDeltaBuilder};
|
|
6
6
|
use pyo3::prelude::*;
|
|
7
7
|
use pyo3::types::{PyDate, PyDateTime, PyDelta, PyDeltaAccess, PyTzInfoAccess};
|
|
8
8
|
|
|
9
|
-
/// Python wrapper for
|
|
9
|
+
/// Python wrapper for dateutil::relativedelta::RelativeDelta.
|
|
10
10
|
#[pyclass(name = "relativedelta", from_py_object)]
|
|
11
11
|
#[derive(Clone, Debug)]
|
|
12
12
|
pub struct PyRelativeDelta {
|
|
@@ -1,37 +1,22 @@
|
|
|
1
1
|
use std::sync::{Arc, Mutex, OnceLock};
|
|
2
2
|
|
|
3
3
|
use super::common::PyWeekday;
|
|
4
|
-
use
|
|
5
|
-
use
|
|
6
|
-
use
|
|
4
|
+
use super::conv::{extract_i32_list, extract_u8_list, py_any_to_naive_datetime};
|
|
5
|
+
use dateutil::common::Weekday;
|
|
6
|
+
use dateutil::rrule::iter::RRuleIter as CoreRRuleIter;
|
|
7
|
+
use dateutil::rrule::{
|
|
7
8
|
search_after, search_before, search_between,
|
|
8
9
|
Frequency, Recurrence, RRule, RRuleBuilder,
|
|
9
10
|
};
|
|
10
|
-
use
|
|
11
|
-
use
|
|
11
|
+
use dateutil::rrule::parse::{rrulestr as core_rrulestr, RRuleStrResult};
|
|
12
|
+
use dateutil::rrule::set::{RRuleSet, RRuleSetIter as CoreRRuleSetIter};
|
|
12
13
|
use pyo3::prelude::*;
|
|
13
14
|
use pyo3::types::{PyAnyMethods, PySlice};
|
|
14
15
|
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
|
-
//
|
|
17
|
+
// Helper: accept weekday scalar-or-sequence for byweekday parameter
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
|
|
19
|
-
/// Accept either a single `i32` or a list of `i32`.
|
|
20
|
-
fn extract_i32_list(obj: &Bound<'_, PyAny>) -> PyResult<Vec<i32>> {
|
|
21
|
-
if let Ok(v) = obj.extract::<i32>() {
|
|
22
|
-
return Ok(vec![v]);
|
|
23
|
-
}
|
|
24
|
-
obj.extract::<Vec<i32>>()
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/// Accept either a single `u8` or a list of `u8`.
|
|
28
|
-
fn extract_u8_list(obj: &Bound<'_, PyAny>) -> PyResult<Vec<u8>> {
|
|
29
|
-
if let Ok(v) = obj.extract::<u8>() {
|
|
30
|
-
return Ok(vec![v]);
|
|
31
|
-
}
|
|
32
|
-
obj.extract::<Vec<u8>>()
|
|
33
|
-
}
|
|
34
|
-
|
|
35
20
|
/// Accept a single weekday/int or a list/tuple of weekday/int and return `Vec<Weekday>`.
|
|
36
21
|
fn extract_byweekday_any(obj: &Bound<'_, PyAny>) -> PyResult<Vec<Weekday>> {
|
|
37
22
|
// Try as a single weekday object
|
|
@@ -128,11 +113,11 @@ impl PyRRule {
|
|
|
128
113
|
fn new(
|
|
129
114
|
py: Python<'_>,
|
|
130
115
|
freq: u8,
|
|
131
|
-
dtstart: Option<
|
|
116
|
+
dtstart: Option<&Bound<'_, PyAny>>,
|
|
132
117
|
interval: u32,
|
|
133
118
|
wkst: Option<Bound<'_, PyAny>>,
|
|
134
119
|
count: Option<u32>,
|
|
135
|
-
until: Option<
|
|
120
|
+
until: Option<&Bound<'_, PyAny>>,
|
|
136
121
|
bysetpos: Option<Bound<'_, PyAny>>,
|
|
137
122
|
bymonth: Option<Bound<'_, PyAny>>,
|
|
138
123
|
bymonthday: Option<Bound<'_, PyAny>>,
|
|
@@ -149,8 +134,8 @@ impl PyRRule {
|
|
|
149
134
|
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
|
|
150
135
|
let mut builder = RRuleBuilder::new(f).interval(interval);
|
|
151
136
|
|
|
152
|
-
if let Some(
|
|
153
|
-
builder = builder.dtstart(
|
|
137
|
+
if let Some(obj) = dtstart {
|
|
138
|
+
builder = builder.dtstart(py_any_to_naive_datetime(obj)?);
|
|
154
139
|
}
|
|
155
140
|
if let Some(ref w) = wkst {
|
|
156
141
|
let val = if let Ok(wd) = w.extract::<PyWeekday>() {
|
|
@@ -176,8 +161,8 @@ impl PyRRule {
|
|
|
176
161
|
if let Some(c) = count {
|
|
177
162
|
builder = builder.count(c);
|
|
178
163
|
}
|
|
179
|
-
if let Some(
|
|
180
|
-
builder = builder.until(
|
|
164
|
+
if let Some(obj) = until {
|
|
165
|
+
builder = builder.until(py_any_to_naive_datetime(obj)?);
|
|
181
166
|
}
|
|
182
167
|
if let Some(ref v) = bysetpos {
|
|
183
168
|
builder = builder.bysetpos(extract_i32_list(v)?);
|
|
@@ -413,6 +398,170 @@ impl PyRRule {
|
|
|
413
398
|
}
|
|
414
399
|
self.inner.contains(dt)
|
|
415
400
|
}
|
|
401
|
+
|
|
402
|
+
/// Return an iterator yielding `count` occurrences after `dt`.
|
|
403
|
+
#[pyo3(signature = (dt, count=1, inc=false))]
|
|
404
|
+
fn xafter(
|
|
405
|
+
&self,
|
|
406
|
+
dt: chrono::NaiveDateTime,
|
|
407
|
+
count: usize,
|
|
408
|
+
inc: bool,
|
|
409
|
+
) -> Vec<chrono::NaiveDateTime> {
|
|
410
|
+
if let Some(cached) = self.get_cache() {
|
|
411
|
+
let start = if inc {
|
|
412
|
+
cached.partition_point(|&x| x < dt)
|
|
413
|
+
} else {
|
|
414
|
+
cached.partition_point(|&x| x <= dt)
|
|
415
|
+
};
|
|
416
|
+
return cached[start..].iter().copied().take(count).collect();
|
|
417
|
+
}
|
|
418
|
+
self.inner
|
|
419
|
+
.iter()
|
|
420
|
+
.filter(move |&i| if inc { i >= dt } else { i > dt })
|
|
421
|
+
.take(count)
|
|
422
|
+
.collect()
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/// Return a new rrule with specified parameters replaced.
|
|
426
|
+
#[pyo3(signature = (
|
|
427
|
+
freq=None,
|
|
428
|
+
dtstart=None,
|
|
429
|
+
interval=None,
|
|
430
|
+
wkst=None,
|
|
431
|
+
count=None,
|
|
432
|
+
until=None,
|
|
433
|
+
bysetpos=None,
|
|
434
|
+
bymonth=None,
|
|
435
|
+
bymonthday=None,
|
|
436
|
+
byyearday=None,
|
|
437
|
+
byeaster=None,
|
|
438
|
+
byweekno=None,
|
|
439
|
+
byweekday=None,
|
|
440
|
+
byhour=None,
|
|
441
|
+
byminute=None,
|
|
442
|
+
bysecond=None,
|
|
443
|
+
cache=None,
|
|
444
|
+
))]
|
|
445
|
+
#[allow(clippy::too_many_arguments)]
|
|
446
|
+
fn replace(
|
|
447
|
+
&self,
|
|
448
|
+
freq: Option<u8>,
|
|
449
|
+
dtstart: Option<&Bound<'_, PyAny>>,
|
|
450
|
+
interval: Option<u32>,
|
|
451
|
+
wkst: Option<Bound<'_, PyAny>>,
|
|
452
|
+
count: Option<u32>,
|
|
453
|
+
until: Option<&Bound<'_, PyAny>>,
|
|
454
|
+
bysetpos: Option<Bound<'_, PyAny>>,
|
|
455
|
+
bymonth: Option<Bound<'_, PyAny>>,
|
|
456
|
+
bymonthday: Option<Bound<'_, PyAny>>,
|
|
457
|
+
byyearday: Option<Bound<'_, PyAny>>,
|
|
458
|
+
byeaster: Option<Bound<'_, PyAny>>,
|
|
459
|
+
byweekno: Option<Bound<'_, PyAny>>,
|
|
460
|
+
byweekday: Option<Bound<'_, PyAny>>,
|
|
461
|
+
byhour: Option<Bound<'_, PyAny>>,
|
|
462
|
+
byminute: Option<Bound<'_, PyAny>>,
|
|
463
|
+
bysecond: Option<Bound<'_, PyAny>>,
|
|
464
|
+
cache: Option<bool>,
|
|
465
|
+
) -> PyResult<Self> {
|
|
466
|
+
let f = if let Some(fv) = freq {
|
|
467
|
+
Frequency::try_from(fv)
|
|
468
|
+
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?
|
|
469
|
+
} else {
|
|
470
|
+
self.inner.freq()
|
|
471
|
+
};
|
|
472
|
+
let mut builder = RRuleBuilder::new(f);
|
|
473
|
+
|
|
474
|
+
builder = builder.dtstart(if let Some(obj) = dtstart {
|
|
475
|
+
py_any_to_naive_datetime(obj)?
|
|
476
|
+
} else {
|
|
477
|
+
self.inner.dtstart()
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
builder = builder.interval(interval.unwrap_or_else(|| self.inner.interval()));
|
|
481
|
+
|
|
482
|
+
let wkst_val = if let Some(ref w) = wkst {
|
|
483
|
+
if let Ok(wd) = w.extract::<PyWeekday>() {
|
|
484
|
+
wd.weekday()
|
|
485
|
+
} else {
|
|
486
|
+
w.extract::<u8>()?
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
self.inner.wkst()
|
|
490
|
+
};
|
|
491
|
+
builder = builder.wkst(wkst_val);
|
|
492
|
+
|
|
493
|
+
if let Some(c) = count.or(self.inner.count()) {
|
|
494
|
+
builder = builder.count(c);
|
|
495
|
+
}
|
|
496
|
+
if let Some(obj) = until {
|
|
497
|
+
builder = builder.until(py_any_to_naive_datetime(obj)?);
|
|
498
|
+
} else if let Some(u) = self.inner.until() {
|
|
499
|
+
builder = builder.until(u);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// by* fields: use provided override, else copy from existing rule
|
|
503
|
+
if let Some(ref v) = bysetpos {
|
|
504
|
+
builder = builder.bysetpos(extract_i32_list(v)?);
|
|
505
|
+
} else if let Some(v) = self.inner.bysetpos() {
|
|
506
|
+
builder = builder.bysetpos(v.to_vec());
|
|
507
|
+
}
|
|
508
|
+
if let Some(ref v) = bymonth {
|
|
509
|
+
builder = builder.bymonth(extract_u8_list(v)?);
|
|
510
|
+
} else if let Some(v) = self.inner.bymonth() {
|
|
511
|
+
builder = builder.bymonth(v.to_vec());
|
|
512
|
+
}
|
|
513
|
+
if let Some(ref v) = bymonthday {
|
|
514
|
+
builder = builder.bymonthday(extract_i32_list(v)?);
|
|
515
|
+
} else if !self.inner.bymonthday().is_empty() {
|
|
516
|
+
builder = builder.bymonthday(self.inner.bymonthday().to_vec());
|
|
517
|
+
}
|
|
518
|
+
if let Some(ref v) = byyearday {
|
|
519
|
+
builder = builder.byyearday(extract_i32_list(v)?);
|
|
520
|
+
} else if let Some(v) = self.inner.byyearday() {
|
|
521
|
+
builder = builder.byyearday(v.to_vec());
|
|
522
|
+
}
|
|
523
|
+
if let Some(ref v) = byeaster {
|
|
524
|
+
builder = builder.byeaster(extract_i32_list(v)?);
|
|
525
|
+
} else if let Some(v) = self.inner.byeaster() {
|
|
526
|
+
builder = builder.byeaster(v.to_vec());
|
|
527
|
+
}
|
|
528
|
+
if let Some(ref v) = byweekno {
|
|
529
|
+
builder = builder.byweekno(extract_i32_list(v)?);
|
|
530
|
+
} else if let Some(v) = self.inner.byweekno() {
|
|
531
|
+
builder = builder.byweekno(v.to_vec());
|
|
532
|
+
}
|
|
533
|
+
if let Some(ref v) = byweekday {
|
|
534
|
+
builder = builder.byweekday(extract_byweekday_any(v)?);
|
|
535
|
+
} else if let Some(v) = self.inner.byweekday() {
|
|
536
|
+
builder = builder.byweekday(v.to_vec());
|
|
537
|
+
}
|
|
538
|
+
if let Some(ref v) = byhour {
|
|
539
|
+
builder = builder.byhour(extract_u8_list(v)?);
|
|
540
|
+
} else if let Some(v) = self.inner.byhour() {
|
|
541
|
+
builder = builder.byhour(v.to_vec());
|
|
542
|
+
}
|
|
543
|
+
if let Some(ref v) = byminute {
|
|
544
|
+
builder = builder.byminute(extract_u8_list(v)?);
|
|
545
|
+
} else if let Some(v) = self.inner.byminute() {
|
|
546
|
+
builder = builder.byminute(v.to_vec());
|
|
547
|
+
}
|
|
548
|
+
if let Some(ref v) = bysecond {
|
|
549
|
+
builder = builder.bysecond(extract_u8_list(v)?);
|
|
550
|
+
} else if let Some(v) = self.inner.bysecond() {
|
|
551
|
+
builder = builder.bysecond(v.to_vec());
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
let inner = builder
|
|
555
|
+
.build()
|
|
556
|
+
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
|
|
557
|
+
let cache_val = cache.unwrap_or(self.cache_enabled);
|
|
558
|
+
let cache_enabled = cache_val && inner.is_finite();
|
|
559
|
+
Ok(Self {
|
|
560
|
+
inner,
|
|
561
|
+
cache_enabled,
|
|
562
|
+
cache: OnceLock::new(),
|
|
563
|
+
})
|
|
564
|
+
}
|
|
416
565
|
}
|
|
417
566
|
|
|
418
567
|
impl PyRRule {
|
|
@@ -874,12 +1023,13 @@ impl PyRRuleSetIter {
|
|
|
874
1023
|
fn rrulestr_py(
|
|
875
1024
|
py: Python<'_>,
|
|
876
1025
|
s: &str,
|
|
877
|
-
dtstart: Option<
|
|
1026
|
+
dtstart: Option<&Bound<'_, PyAny>>,
|
|
878
1027
|
forceset: bool,
|
|
879
1028
|
compatible: bool,
|
|
880
1029
|
unfold: bool,
|
|
881
1030
|
cache: bool,
|
|
882
1031
|
) -> PyResult<Py<PyAny>> {
|
|
1032
|
+
let dtstart = dtstart.map(py_any_to_naive_datetime).transpose()?;
|
|
883
1033
|
let result = core_rrulestr(s, dtstart, forceset, compatible, unfold)
|
|
884
1034
|
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
|
|
885
1035
|
match result {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
use chrono::NaiveDateTime;
|
|
2
|
-
use
|
|
2
|
+
use dateutil::tz::{self, TimeZone, TzFile, TzLocal, TzOffset, TzOps};
|
|
3
3
|
use pyo3::prelude::*;
|
|
4
4
|
use pyo3::types::{PyDateTime, PyDelta, PyTzInfo};
|
|
5
5
|
|
|
@@ -315,43 +315,83 @@ fn gettz_py(py: Python<'_>, name: Option<&str>) -> PyResult<Py<PyAny>> {
|
|
|
315
315
|
|
|
316
316
|
#[derive(FromPyObject)]
|
|
317
317
|
enum PyTimezone<'py> {
|
|
318
|
+
#[allow(dead_code)]
|
|
318
319
|
Utc(PyRef<'py, PyTzUtc>),
|
|
319
320
|
Offset(PyRef<'py, PyTzOffset>),
|
|
320
321
|
File(PyRef<'py, PyTzFile>),
|
|
321
322
|
Local(PyRef<'py, PyTzLocal>),
|
|
322
323
|
}
|
|
323
324
|
|
|
324
|
-
impl PyTimezone<'_> {
|
|
325
|
-
|
|
325
|
+
impl TzOps for PyTimezone<'_> {
|
|
326
|
+
#[inline]
|
|
327
|
+
fn utcoffset(&self, dt: NaiveDateTime, fold: bool) -> i32 {
|
|
326
328
|
match self {
|
|
327
|
-
PyTimezone::Utc(
|
|
328
|
-
PyTimezone::Offset(tz) =>
|
|
329
|
-
PyTimezone::File(tz) =>
|
|
330
|
-
PyTimezone::Local(tz) =>
|
|
329
|
+
PyTimezone::Utc(_) => 0,
|
|
330
|
+
PyTimezone::Offset(tz) => tz.inner.offset_seconds(),
|
|
331
|
+
PyTimezone::File(tz) => tz.inner.utcoffset(dt, fold),
|
|
332
|
+
PyTimezone::Local(tz) => tz.inner.utcoffset(dt, fold),
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#[inline]
|
|
337
|
+
fn dst(&self, dt: NaiveDateTime, fold: bool) -> i32 {
|
|
338
|
+
match self {
|
|
339
|
+
PyTimezone::Utc(_) | PyTimezone::Offset(_) => 0,
|
|
340
|
+
PyTimezone::File(tz) => tz.inner.dst(dt, fold),
|
|
341
|
+
PyTimezone::Local(tz) => tz.inner.dst(dt, fold),
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#[inline]
|
|
346
|
+
fn tzname(&self, dt: NaiveDateTime, fold: bool) -> &str {
|
|
347
|
+
match self {
|
|
348
|
+
PyTimezone::Utc(_) => "UTC",
|
|
349
|
+
PyTimezone::Offset(tz) => tz.inner.display_name(),
|
|
350
|
+
PyTimezone::File(tz) => tz.inner.tzname(dt, fold),
|
|
351
|
+
PyTimezone::Local(tz) => tz.inner.tzname(dt, fold),
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
#[inline]
|
|
356
|
+
fn is_ambiguous(&self, dt: NaiveDateTime) -> bool {
|
|
357
|
+
match self {
|
|
358
|
+
PyTimezone::Utc(_) | PyTimezone::Offset(_) => false,
|
|
359
|
+
PyTimezone::File(tz) => tz.inner.is_ambiguous(dt),
|
|
360
|
+
PyTimezone::Local(tz) => tz.inner.is_ambiguous(dt),
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
#[inline]
|
|
365
|
+
fn fromutc(&self, dt: NaiveDateTime) -> NaiveDateTime {
|
|
366
|
+
match self {
|
|
367
|
+
PyTimezone::Utc(_) => dt,
|
|
368
|
+
PyTimezone::Offset(tz) => tz.inner.fromutc(dt),
|
|
369
|
+
PyTimezone::File(tz) => tz.inner.fromutc(dt),
|
|
370
|
+
PyTimezone::Local(tz) => tz.inner.fromutc(dt),
|
|
331
371
|
}
|
|
332
372
|
}
|
|
333
373
|
}
|
|
334
374
|
|
|
335
375
|
// ---------------------------------------------------------------------------
|
|
336
|
-
// Helper functions
|
|
376
|
+
// Helper functions — zero-clone via TzOps trait
|
|
337
377
|
// ---------------------------------------------------------------------------
|
|
338
378
|
|
|
339
379
|
#[pyfunction]
|
|
340
380
|
#[pyo3(name = "datetime_exists")]
|
|
341
381
|
fn datetime_exists_py(dt: NaiveDateTime, tz: PyTimezone<'_>) -> bool {
|
|
342
|
-
tz::datetime_exists(dt, &tz
|
|
382
|
+
tz::datetime_exists(dt, &tz)
|
|
343
383
|
}
|
|
344
384
|
|
|
345
385
|
#[pyfunction]
|
|
346
386
|
#[pyo3(name = "datetime_ambiguous")]
|
|
347
387
|
fn datetime_ambiguous_py(dt: NaiveDateTime, tz: PyTimezone<'_>) -> bool {
|
|
348
|
-
tz::datetime_ambiguous(dt, &tz
|
|
388
|
+
tz::datetime_ambiguous(dt, &tz)
|
|
349
389
|
}
|
|
350
390
|
|
|
351
391
|
#[pyfunction]
|
|
352
392
|
#[pyo3(name = "resolve_imaginary")]
|
|
353
393
|
fn resolve_imaginary_py(dt: NaiveDateTime, tz: PyTimezone<'_>) -> NaiveDateTime {
|
|
354
|
-
tz::resolve_imaginary(dt, &tz
|
|
394
|
+
tz::resolve_imaginary(dt, &tz)
|
|
355
395
|
}
|
|
356
396
|
|
|
357
397
|
// ---------------------------------------------------------------------------
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
[package]
|
|
2
|
-
name = "dateutil-core"
|
|
3
|
-
version = "0.1.1"
|
|
4
|
-
edition = "2021"
|
|
5
|
-
license = "MIT"
|
|
6
|
-
description = "Performance-optimized date utility library for Rust"
|
|
7
|
-
|
|
8
|
-
[lib]
|
|
9
|
-
name = "dateutil_core"
|
|
10
|
-
crate-type = ["rlib"]
|
|
11
|
-
|
|
12
|
-
[dependencies]
|
|
13
|
-
bitflags = "2"
|
|
14
|
-
chrono = "0.4"
|
|
15
|
-
phf = { version = "0.13", features = ["macros"] }
|
|
16
|
-
smallvec = "1.15"
|
|
17
|
-
thiserror = "2"
|
|
18
|
-
iana-time-zone = "0.1"
|
|
19
|
-
|
|
20
|
-
[dev-dependencies]
|
|
21
|
-
criterion = { version = "0.8", features = ["html_reports"] }
|
|
22
|
-
|
|
23
|
-
[[bench]]
|
|
24
|
-
name = "benchmarks"
|
|
25
|
-
harness = false
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/parser/isoparser.rs
RENAMED
|
File without changes
|
{python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/parser/parserinfo.rs
RENAMED
|
File without changes
|
{python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/parser/tokenizer.rs
RENAMED
|
File without changes
|
|
File without changes
|
{python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/relativedelta.rs
RENAMED
|
File without changes
|
{python_dateutil_rs-0.1.1 → python_dateutil_rs-0.1.2}/crates/dateutil-core/src/rrule/iter.rs
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|