python-dateutil-rs 0.1.2__tar.gz → 0.1.3__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.2 → python_dateutil_rs-0.1.3}/Cargo.lock +2 -2
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/PKG-INFO +1 -1
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/Cargo.toml +1 -1
- python_dateutil_rs-0.1.3/crates/dateutil-core/README.md +167 -0
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/benches/benchmarks.rs +57 -18
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/common.rs +70 -6
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/easter.rs +195 -50
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/error.rs +34 -8
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/parser/isoparser.rs +23 -16
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/parser/parserinfo.rs +50 -21
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/parser/tokenizer.rs +7 -2
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/parser.rs +193 -86
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/relativedelta.rs +539 -215
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/rrule/iter.rs +15 -12
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/rrule/parse.rs +27 -71
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/rrule/set.rs +21 -60
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/rrule.rs +238 -91
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/tz/file.rs +55 -75
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/tz/local.rs +5 -6
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/tz/offset.rs +1 -5
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/tz/utc.rs +1 -5
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/tz.rs +90 -31
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-py/Cargo.toml +1 -1
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-py/src/py/common.rs +7 -4
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-py/src/py/conv.rs +7 -21
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-py/src/py/easter.rs +1 -2
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-py/src/py/parser.rs +13 -14
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-py/src/py/relativedelta.rs +117 -62
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-py/src/py/rrule.rs +107 -233
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-py/src/py/tz.rs +3 -4
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/pyproject.toml +1 -1
- python_dateutil_rs-0.1.2/crates/dateutil-core/README.md +0 -209
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/Cargo.toml +0 -0
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/LICENSE +0 -0
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/README.md +0 -0
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/CLAUDE.md +0 -0
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/src/lib.rs +0 -0
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-py/src/lib.rs +0 -0
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-py/src/py.rs +0 -0
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/python/dateutil_rs/__init__.py +0 -0
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/python/dateutil_rs/_native.pyi +0 -0
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/python/dateutil_rs/easter.py +0 -0
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/python/dateutil_rs/parser.py +0 -0
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/python/dateutil_rs/py.typed +0 -0
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/python/dateutil_rs/relativedelta.py +0 -0
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/python/dateutil_rs/rrule.py +0 -0
- {python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/python/dateutil_rs/tz.py +0 -0
|
@@ -220,7 +220,7 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
|
|
220
220
|
|
|
221
221
|
[[package]]
|
|
222
222
|
name = "dateutil"
|
|
223
|
-
version = "0.1.
|
|
223
|
+
version = "0.1.3"
|
|
224
224
|
dependencies = [
|
|
225
225
|
"bitflags",
|
|
226
226
|
"chrono",
|
|
@@ -234,7 +234,7 @@ dependencies = [
|
|
|
234
234
|
|
|
235
235
|
[[package]]
|
|
236
236
|
name = "dateutil-py"
|
|
237
|
-
version = "0.1.
|
|
237
|
+
version = "0.1.3"
|
|
238
238
|
dependencies = [
|
|
239
239
|
"chrono",
|
|
240
240
|
"dateutil",
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# dateutil
|
|
2
|
+
|
|
3
|
+
[](https://crates.io/crates/dateutil)
|
|
4
|
+
[](https://docs.rs/dateutil)
|
|
5
|
+
[](https://crates.io/crates/dateutil)
|
|
6
|
+
[](https://github.com/wakita181009/dateutil-rs/actions/workflows/ci.yml)
|
|
7
|
+
|
|
8
|
+
Fast date utility library for Rust — parser, relativedelta, rrule, timezone.
|
|
9
|
+
|
|
10
|
+
A performance-focused Rust reimplementation of [python-dateutil](https://github.com/dateutil/dateutil), designed for native Rust usage. Also available as a Python package ([python-dateutil-rs](https://pypi.org/project/python-dateutil-rs/)) via PyO3.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Parser** — Parse human-readable date strings with a zero-copy tokenizer and PHF lookup tables
|
|
15
|
+
- **ISO 8601** — Strict ISO-8601 parsing via `isoparse()`
|
|
16
|
+
- **RelativeDelta** — Relative date arithmetic (months, years, weekdays, etc.)
|
|
17
|
+
- **RRule / RRuleSet** — RFC 5545 recurrence rules with bitflag filters and buffer-reusing iteration
|
|
18
|
+
- **Timezone** — `gettz()` with TZif file support, DST handling, and process-lifetime caching
|
|
19
|
+
- **Easter** — Easter date calculation (Julian, Orthodox, Western)
|
|
20
|
+
- **Weekday** — `MO`–`SU` constants with N-th occurrence support
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```toml
|
|
25
|
+
[dependencies]
|
|
26
|
+
dateutil = "0.1"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
### Parsing date strings
|
|
32
|
+
|
|
33
|
+
```rust
|
|
34
|
+
use chrono::NaiveDate;
|
|
35
|
+
use dateutil::parser;
|
|
36
|
+
|
|
37
|
+
// Parse a human-readable date string
|
|
38
|
+
let dt = parser::parse("January 15, 2026 10:30 AM", None, None, false)
|
|
39
|
+
.unwrap();
|
|
40
|
+
assert_eq!(dt.date(), NaiveDate::from_ymd_opt(2026, 1, 15).unwrap());
|
|
41
|
+
|
|
42
|
+
// ISO-8601 strict parsing
|
|
43
|
+
use dateutil::parser::isoparser::isoparse;
|
|
44
|
+
let dt = isoparse("2026-01-15T10:30:00").unwrap();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Relative deltas
|
|
48
|
+
|
|
49
|
+
```rust
|
|
50
|
+
use chrono::{NaiveDate, NaiveDateTime};
|
|
51
|
+
use dateutil::relativedelta::RelativeDelta;
|
|
52
|
+
|
|
53
|
+
let dt = NaiveDate::from_ymd_opt(2026, 1, 31).unwrap().and_hms_opt(0, 0, 0).unwrap();
|
|
54
|
+
let rd = RelativeDelta::new().months(1).build();
|
|
55
|
+
|
|
56
|
+
// Jan 31 + 1 month = Feb 28 (clamped)
|
|
57
|
+
let result = rd.add_to_datetime(dt);
|
|
58
|
+
assert_eq!(result.date(), NaiveDate::from_ymd_opt(2026, 2, 28).unwrap());
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Recurrence rules
|
|
62
|
+
|
|
63
|
+
```rust
|
|
64
|
+
use chrono::NaiveDate;
|
|
65
|
+
use dateutil::rrule::{Frequency, Recurrence, RRuleBuilder};
|
|
66
|
+
|
|
67
|
+
let dtstart = NaiveDate::from_ymd_opt(2026, 1, 1).unwrap()
|
|
68
|
+
.and_hms_opt(0, 0, 0).unwrap();
|
|
69
|
+
|
|
70
|
+
let rule = RRuleBuilder::new(Frequency::Monthly)
|
|
71
|
+
.dtstart(dtstart)
|
|
72
|
+
.count(3)
|
|
73
|
+
.build()
|
|
74
|
+
.unwrap();
|
|
75
|
+
|
|
76
|
+
let dates = rule.all();
|
|
77
|
+
assert_eq!(dates.len(), 3);
|
|
78
|
+
|
|
79
|
+
// Also works as an iterator
|
|
80
|
+
for dt in rule.iter().take(3) {
|
|
81
|
+
println!("{}", dt);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Parsing RFC 5545 RRULE strings
|
|
86
|
+
|
|
87
|
+
```rust
|
|
88
|
+
use dateutil::rrule::parse::rrulestr;
|
|
89
|
+
|
|
90
|
+
let result = rrulestr(
|
|
91
|
+
"DTSTART:20260101T000000\nRRULE:FREQ=WEEKLY;COUNT=4;BYDAY=MO,WE,FR",
|
|
92
|
+
false,
|
|
93
|
+
).unwrap();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Timezones
|
|
97
|
+
|
|
98
|
+
```rust
|
|
99
|
+
use chrono::NaiveDate;
|
|
100
|
+
use dateutil::tz::{self, TzOps};
|
|
101
|
+
|
|
102
|
+
// Look up an IANA timezone (cached after first call)
|
|
103
|
+
let tz = tz::gettz(Some("America/New_York")).unwrap();
|
|
104
|
+
|
|
105
|
+
let dt = NaiveDate::from_ymd_opt(2026, 7, 15).unwrap()
|
|
106
|
+
.and_hms_opt(12, 0, 0).unwrap();
|
|
107
|
+
|
|
108
|
+
// UTC offset in seconds (EDT = -4h)
|
|
109
|
+
assert_eq!(tz.utcoffset(dt, false), -4 * 3600);
|
|
110
|
+
|
|
111
|
+
// DST gap/overlap utilities
|
|
112
|
+
assert!(tz::datetime_exists(dt, &tz));
|
|
113
|
+
assert!(!tz::datetime_ambiguous(dt, &tz));
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Easter
|
|
117
|
+
|
|
118
|
+
```rust
|
|
119
|
+
use chrono::NaiveDate;
|
|
120
|
+
use dateutil::easter::{easter, EasterMethod};
|
|
121
|
+
|
|
122
|
+
let date = easter(2026, EasterMethod::Western).unwrap();
|
|
123
|
+
assert_eq!(date, NaiveDate::from_ymd_opt(2026, 4, 5).unwrap());
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Weekday constants
|
|
127
|
+
|
|
128
|
+
```rust
|
|
129
|
+
use dateutil::common::{MO, TU, FR};
|
|
130
|
+
|
|
131
|
+
// N-th occurrence (e.g., 2nd Tuesday)
|
|
132
|
+
let second_tue = TU.nth(2);
|
|
133
|
+
assert_eq!(second_tue.n(), Some(2));
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Modules
|
|
137
|
+
|
|
138
|
+
| Module | Description |
|
|
139
|
+
|--------|-------------|
|
|
140
|
+
| `dateutil::parser` | Date string parsing (`parse`, `isoparse`, `parse_to_result`) |
|
|
141
|
+
| `dateutil::relativedelta` | Relative date arithmetic (`RelativeDelta`, `RelativeDeltaBuilder`) |
|
|
142
|
+
| `dateutil::rrule` | RFC 5545 recurrence rules (`RRule`, `RRuleBuilder`, `Recurrence`) |
|
|
143
|
+
| `dateutil::rrule::set` | Recurrence rule sets (`RRuleSet`) |
|
|
144
|
+
| `dateutil::rrule::parse` | RRULE string parsing (`rrulestr`) |
|
|
145
|
+
| `dateutil::tz` | Timezones (`gettz`, `TzOps`, `TimeZone`, `TzFile`, `TzOffset`, `TzUtc`, `TzLocal`) |
|
|
146
|
+
| `dateutil::easter` | Easter calculation (`easter`, `EasterMethod`) |
|
|
147
|
+
| `dateutil::common` | Weekday constants (`MO`–`SU`, `Weekday`) |
|
|
148
|
+
| `dateutil::error` | Error types (`ParseError`, `RRuleError`, `TzError`, etc.) |
|
|
149
|
+
|
|
150
|
+
## Performance
|
|
151
|
+
|
|
152
|
+
Benchmarked against python-dateutil (via PyO3 bindings):
|
|
153
|
+
|
|
154
|
+
| Module | Speedup |
|
|
155
|
+
|--------|---------|
|
|
156
|
+
| Parser (parse) | 19.5x–36.0x |
|
|
157
|
+
| Parser (isoparse) | 13.0x–38.4x |
|
|
158
|
+
| RRule | 5.9x–63.7x |
|
|
159
|
+
| Timezone | 1.0x–896.7x |
|
|
160
|
+
| RelativeDelta | 2.0x–28.1x |
|
|
161
|
+
| Easter | 5.0x–7.3x |
|
|
162
|
+
|
|
163
|
+
Key optimizations: zero-copy tokenizer, PHF compile-time hash tables, bitflag-based filters, `SmallVec` buffer reuse, `FxHashMap` timezone cache, `TzOps` trait for generic zero-clone dispatch.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
[MIT](../../LICENSE)
|
{python_dateutil_rs-0.1.2 → python_dateutil_rs-0.1.3}/crates/dateutil-core/benches/benchmarks.rs
RENAMED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
use criterion::{criterion_group, criterion_main, Criterion};
|
|
2
|
-
use std::hint::black_box;
|
|
3
1
|
use chrono::NaiveDate;
|
|
2
|
+
use criterion::{criterion_group, criterion_main, Criterion};
|
|
4
3
|
use dateutil::common::Weekday;
|
|
5
4
|
use dateutil::easter::{easter, EasterMethod};
|
|
6
5
|
use dateutil::parser;
|
|
7
6
|
use dateutil::parser::tokenizer;
|
|
8
7
|
use dateutil::relativedelta::RelativeDelta;
|
|
9
|
-
use dateutil::rrule::{Frequency, Recurrence, RRuleBuilder};
|
|
10
8
|
use dateutil::rrule::parse::rrulestr;
|
|
11
9
|
use dateutil::rrule::set::RRuleSet;
|
|
10
|
+
use dateutil::rrule::{Frequency, RRuleBuilder, Recurrence};
|
|
11
|
+
use std::hint::black_box;
|
|
12
12
|
|
|
13
13
|
fn bench_tokenizer(c: &mut Criterion) {
|
|
14
14
|
c.bench_function("tokenize_simple_date", |b| {
|
|
@@ -47,21 +47,31 @@ fn bench_parser(c: &mut Criterion) {
|
|
|
47
47
|
|
|
48
48
|
c.bench_function("parse_datetime", |b| {
|
|
49
49
|
b.iter(|| {
|
|
50
|
-
black_box(
|
|
50
|
+
black_box(
|
|
51
|
+
parser::parse(black_box("2024-01-15 10:30:45"), false, false, None, None).unwrap(),
|
|
52
|
+
);
|
|
51
53
|
})
|
|
52
54
|
});
|
|
53
55
|
|
|
54
56
|
c.bench_function("parse_month_name", |b| {
|
|
55
57
|
b.iter(|| {
|
|
56
|
-
black_box(
|
|
58
|
+
black_box(
|
|
59
|
+
parser::parse(black_box("January 15, 2024"), false, false, None, None).unwrap(),
|
|
60
|
+
);
|
|
57
61
|
})
|
|
58
62
|
});
|
|
59
63
|
|
|
60
64
|
c.bench_function("parse_complex", |b| {
|
|
61
65
|
b.iter(|| {
|
|
62
66
|
black_box(
|
|
63
|
-
parser::parse(
|
|
64
|
-
.
|
|
67
|
+
parser::parse(
|
|
68
|
+
black_box("Monday, January 15, 2024 3:30:45.123456 PM UTC"),
|
|
69
|
+
false,
|
|
70
|
+
false,
|
|
71
|
+
None,
|
|
72
|
+
None,
|
|
73
|
+
)
|
|
74
|
+
.unwrap(),
|
|
65
75
|
);
|
|
66
76
|
})
|
|
67
77
|
});
|
|
@@ -71,8 +81,11 @@ fn bench_parser(c: &mut Criterion) {
|
|
|
71
81
|
black_box(
|
|
72
82
|
parser::parse_to_result(
|
|
73
83
|
black_box("Monday, January 15, 2024 3:30:45.123456 PM EST -05:00"),
|
|
74
|
-
false,
|
|
75
|
-
|
|
84
|
+
false,
|
|
85
|
+
false,
|
|
86
|
+
None,
|
|
87
|
+
)
|
|
88
|
+
.unwrap(),
|
|
76
89
|
);
|
|
77
90
|
})
|
|
78
91
|
});
|
|
@@ -97,7 +110,16 @@ fn bench_parser(c: &mut Criterion) {
|
|
|
97
110
|
|
|
98
111
|
c.bench_function("parse_ampm", |b| {
|
|
99
112
|
b.iter(|| {
|
|
100
|
-
black_box(
|
|
113
|
+
black_box(
|
|
114
|
+
parser::parse(
|
|
115
|
+
black_box("January 15, 2024 3:30 PM"),
|
|
116
|
+
false,
|
|
117
|
+
false,
|
|
118
|
+
None,
|
|
119
|
+
None,
|
|
120
|
+
)
|
|
121
|
+
.unwrap(),
|
|
122
|
+
);
|
|
101
123
|
})
|
|
102
124
|
});
|
|
103
125
|
|
|
@@ -203,8 +225,13 @@ fn bench_relativedelta(c: &mut Criterion) {
|
|
|
203
225
|
c.bench_function("relativedelta_add_complex", |b| {
|
|
204
226
|
let wd = Weekday::new(4, Some(2)).unwrap();
|
|
205
227
|
let delta = RelativeDelta::builder()
|
|
206
|
-
.years(1)
|
|
207
|
-
.
|
|
228
|
+
.years(1)
|
|
229
|
+
.months(2)
|
|
230
|
+
.days(3)
|
|
231
|
+
.hours(4)
|
|
232
|
+
.minutes(30)
|
|
233
|
+
.seconds(15)
|
|
234
|
+
.microseconds(500_000)
|
|
208
235
|
.weekday(wd)
|
|
209
236
|
.build()
|
|
210
237
|
.unwrap();
|
|
@@ -438,8 +465,12 @@ fn bench_rrulestr(c: &mut Criterion) {
|
|
|
438
465
|
black_box(
|
|
439
466
|
rrulestr(
|
|
440
467
|
black_box("DTSTART:20200101T090000\nRRULE:FREQ=DAILY;COUNT=30"),
|
|
441
|
-
None,
|
|
442
|
-
|
|
468
|
+
None,
|
|
469
|
+
false,
|
|
470
|
+
false,
|
|
471
|
+
false,
|
|
472
|
+
)
|
|
473
|
+
.unwrap(),
|
|
443
474
|
);
|
|
444
475
|
})
|
|
445
476
|
});
|
|
@@ -449,8 +480,12 @@ fn bench_rrulestr(c: &mut Criterion) {
|
|
|
449
480
|
black_box(
|
|
450
481
|
rrulestr(
|
|
451
482
|
black_box("DTSTART:20200101T090000\nRRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=52"),
|
|
452
|
-
None,
|
|
453
|
-
|
|
483
|
+
None,
|
|
484
|
+
false,
|
|
485
|
+
false,
|
|
486
|
+
false,
|
|
487
|
+
)
|
|
488
|
+
.unwrap(),
|
|
454
489
|
);
|
|
455
490
|
})
|
|
456
491
|
});
|
|
@@ -481,8 +516,12 @@ fn bench_rrulestr(c: &mut Criterion) {
|
|
|
481
516
|
b.iter(|| {
|
|
482
517
|
let result = rrulestr(
|
|
483
518
|
black_box("DTSTART:20200101T090000\nRRULE:FREQ=DAILY;COUNT=100"),
|
|
484
|
-
None,
|
|
485
|
-
|
|
519
|
+
None,
|
|
520
|
+
false,
|
|
521
|
+
false,
|
|
522
|
+
false,
|
|
523
|
+
)
|
|
524
|
+
.unwrap();
|
|
486
525
|
black_box(result.all());
|
|
487
526
|
})
|
|
488
527
|
});
|
|
@@ -97,6 +97,46 @@ pub const SU: Weekday = Weekday {
|
|
|
97
97
|
n: None,
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Calendar helpers
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/// Returns `true` if `year` is a leap year (Gregorian calendar).
|
|
105
|
+
#[inline]
|
|
106
|
+
pub(crate) fn is_leap_year(year: i32) -> bool {
|
|
107
|
+
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Returns the number of days in the given `month` of `year`.
|
|
111
|
+
///
|
|
112
|
+
/// `month` must be in `1..=12`; out-of-range values return `0`.
|
|
113
|
+
#[inline]
|
|
114
|
+
pub(crate) fn days_in_month(year: i32, month: u32) -> u32 {
|
|
115
|
+
match month {
|
|
116
|
+
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
|
|
117
|
+
4 | 6 | 9 | 11 => 30,
|
|
118
|
+
2 => {
|
|
119
|
+
if is_leap_year(year) {
|
|
120
|
+
29
|
|
121
|
+
} else {
|
|
122
|
+
28
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
_ => 0,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Create a `NaiveDateTime` from year, month, day, hour, minute, second.
|
|
130
|
+
///
|
|
131
|
+
/// Panics on invalid inputs — intended for test code only.
|
|
132
|
+
#[cfg(test)]
|
|
133
|
+
pub(crate) fn dt(y: i32, m: u32, d: u32, h: u32, mi: u32, s: u32) -> chrono::NaiveDateTime {
|
|
134
|
+
chrono::NaiveDate::from_ymd_opt(y, m, d)
|
|
135
|
+
.unwrap()
|
|
136
|
+
.and_hms_opt(h, mi, s)
|
|
137
|
+
.unwrap()
|
|
138
|
+
}
|
|
139
|
+
|
|
100
140
|
#[cfg(test)]
|
|
101
141
|
mod tests {
|
|
102
142
|
use super::*;
|
|
@@ -213,12 +253,6 @@ mod tests {
|
|
|
213
253
|
assert_eq!(set.len(), 2);
|
|
214
254
|
}
|
|
215
255
|
|
|
216
|
-
#[test]
|
|
217
|
-
fn test_weekday_error_display() {
|
|
218
|
-
let err = Weekday::new(7, None).unwrap_err();
|
|
219
|
-
assert_eq!(err.to_string(), "invalid weekday: 7 (must be 0..=6)");
|
|
220
|
-
}
|
|
221
|
-
|
|
222
256
|
#[test]
|
|
223
257
|
fn test_weekday_i32_min_max_n() {
|
|
224
258
|
let wd = Weekday::new(0, Some(i32::MAX)).unwrap();
|
|
@@ -270,4 +304,34 @@ mod tests {
|
|
|
270
304
|
let result: Result<Weekday, _> = 7u8.try_into();
|
|
271
305
|
assert!(result.is_err());
|
|
272
306
|
}
|
|
307
|
+
|
|
308
|
+
// -----------------------------------------------------------------------
|
|
309
|
+
// Calendar helpers
|
|
310
|
+
// -----------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
#[test]
|
|
313
|
+
fn test_is_leap_year() {
|
|
314
|
+
assert!(is_leap_year(2024));
|
|
315
|
+
assert!(!is_leap_year(2023));
|
|
316
|
+
assert!(!is_leap_year(1900)); // century non-leap
|
|
317
|
+
assert!(is_leap_year(2000)); // 400-year leap
|
|
318
|
+
assert!(!is_leap_year(2100)); // century non-leap
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#[test]
|
|
322
|
+
fn test_days_in_month() {
|
|
323
|
+
assert_eq!(days_in_month(2024, 1), 31);
|
|
324
|
+
assert_eq!(days_in_month(2024, 2), 29); // leap year
|
|
325
|
+
assert_eq!(days_in_month(2023, 2), 28); // non-leap
|
|
326
|
+
assert_eq!(days_in_month(2024, 4), 30);
|
|
327
|
+
assert_eq!(days_in_month(2024, 12), 31);
|
|
328
|
+
assert_eq!(days_in_month(1900, 2), 28);
|
|
329
|
+
assert_eq!(days_in_month(2000, 2), 29);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
#[test]
|
|
333
|
+
fn test_days_in_month_invalid() {
|
|
334
|
+
assert_eq!(days_in_month(2024, 0), 0);
|
|
335
|
+
assert_eq!(days_in_month(2024, 13), 0);
|
|
336
|
+
}
|
|
273
337
|
}
|