python-dateutil-rs 0.1.0__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 (45) hide show
  1. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/Cargo.lock +2 -2
  2. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/PKG-INFO +1 -1
  3. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/Cargo.toml +1 -1
  4. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/common.rs +13 -40
  5. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/easter.rs +0 -44
  6. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/parser/isoparser.rs +21 -0
  7. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/parser/parserinfo.rs +40 -0
  8. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/parser.rs +180 -82
  9. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/relativedelta.rs +152 -0
  10. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/rrule/parse.rs +25 -0
  11. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/rrule/set.rs +38 -0
  12. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/rrule.rs +221 -0
  13. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/tz/offset.rs +12 -0
  14. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/tz.rs +38 -14
  15. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-py/Cargo.toml +1 -1
  16. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/pyproject.toml +1 -1
  17. python_dateutil_rs-0.1.1/python/dateutil_rs/easter.py +15 -0
  18. python_dateutil_rs-0.1.1/python/dateutil_rs/relativedelta.py +5 -0
  19. python_dateutil_rs-0.1.1/python/dateutil_rs/rrule.py +27 -0
  20. python_dateutil_rs-0.1.1/python/dateutil_rs/tz.py +23 -0
  21. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/Cargo.toml +0 -0
  22. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/LICENSE +0 -0
  23. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/README.md +0 -0
  24. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/CLAUDE.md +0 -0
  25. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/benches/benchmarks.rs +0 -0
  26. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/error.rs +0 -0
  27. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/lib.rs +0 -0
  28. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/parser/tokenizer.rs +0 -0
  29. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/rrule/iter.rs +0 -0
  30. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/tz/file.rs +0 -0
  31. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/tz/local.rs +0 -0
  32. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-core/src/tz/utc.rs +0 -0
  33. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/lib.rs +0 -0
  34. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/py/common.rs +0 -0
  35. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/py/conv.rs +0 -0
  36. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/py/easter.rs +0 -0
  37. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/py/parser.rs +0 -0
  38. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/py/relativedelta.rs +0 -0
  39. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/py/rrule.rs +0 -0
  40. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/py/tz.rs +0 -0
  41. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/crates/dateutil-py/src/py.rs +0 -0
  42. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/python/dateutil_rs/__init__.py +0 -0
  43. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/python/dateutil_rs/_native.pyi +0 -0
  44. {python_dateutil_rs-0.1.0 → python_dateutil_rs-0.1.1}/python/dateutil_rs/parser.py +0 -0
  45. {python_dateutil_rs-0.1.0 → 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,7 +233,7 @@ 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",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-dateutil-rs
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Classifier: Programming Language :: Python :: 3.10
5
5
  Classifier: Programming Language :: Python :: 3.11
6
6
  Classifier: Programming Language :: Python :: 3.12
@@ -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"
@@ -189,30 +189,6 @@ mod tests {
189
189
  assert_eq!(wd_neg.to_string(), "SU(-100)");
190
190
  }
191
191
 
192
- #[test]
193
- fn test_weekday_negative_one_n() {
194
- // Last occurrence (e.g., last Friday of month)
195
- let wd = FR.with_n(Some(-1));
196
- assert_eq!(wd.n(), Some(-1));
197
- assert_eq!(wd.to_string(), "FR(-1)");
198
- }
199
-
200
- #[test]
201
- fn test_weekday_clone_copy() {
202
- let wd = MO.with_n(Some(2));
203
- let cloned = wd;
204
- assert_eq!(wd, cloned); // Copy semantics — both usable
205
- }
206
-
207
- #[test]
208
- fn test_weekday_boundary_values() {
209
- // Weekday 0 (Monday) and 6 (Sunday) are boundaries
210
- let mon = Weekday::new(0, Some(1)).unwrap();
211
- let sun = Weekday::new(6, Some(-1)).unwrap();
212
- assert_eq!(mon.weekday(), 0);
213
- assert_eq!(sun.weekday(), 6);
214
- }
215
-
216
192
  #[test]
217
193
  fn test_weekday_all_invalid() {
218
194
  for i in 7..=255 {
@@ -227,14 +203,6 @@ mod tests {
227
203
  assert_ne!(a, b);
228
204
  }
229
205
 
230
- #[test]
231
- fn test_weekday_eq_none_vs_zero() {
232
- // n=0 is now rejected at construction time (Weekday::new).
233
- // with_n() bypasses validation for internal use, so test display.
234
- let a = MO.with_n(None);
235
- assert_eq!(a.to_string(), "MO");
236
- }
237
-
238
206
  #[test]
239
207
  fn test_weekday_hash_with_n() {
240
208
  use std::collections::HashSet;
@@ -281,14 +249,6 @@ mod tests {
281
249
  }
282
250
  }
283
251
 
284
- #[test]
285
- fn test_weekday_equality_ignores_n_for_same_display() {
286
- let a = MO.with_n(Some(1));
287
- let b = MO.with_n(Some(-1));
288
- assert_ne!(a, b);
289
- assert_eq!(a.weekday(), b.weekday());
290
- }
291
-
292
252
  #[test]
293
253
  fn test_weekday_hash_set_none() {
294
254
  use std::collections::HashSet;
@@ -297,4 +257,17 @@ mod tests {
297
257
  set.insert(MO.with_n(Some(1)));
298
258
  assert_eq!(set.len(), 2);
299
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
+ }
300
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
@@ -591,4 +591,25 @@ mod tests {
591
591
  fn test_iso_leap_second_rejected() {
592
592
  assert!(isoparse("2024-01-15T23:59:60").is_err());
593
593
  }
594
+
595
+ #[test]
596
+ fn test_iso_hhmm_colon_format() {
597
+ let dt = isoparse("2024-01-15T10:30").unwrap();
598
+ assert_eq!(dt.hour(), 10);
599
+ assert_eq!(dt.minute(), 30);
600
+ assert_eq!(dt.second(), 0);
601
+ }
602
+
603
+ #[test]
604
+ fn test_iso_unrecognized_time_format() {
605
+ let result = isoparse("2024-01-15TX");
606
+ assert!(result.is_err());
607
+ }
608
+
609
+ #[test]
610
+ fn test_iso_fractional_dot_only() {
611
+ let dt = isoparse("2024-01-15T10:30:45.").unwrap();
612
+ assert_eq!(dt.second(), 45);
613
+ assert_eq!(dt.nanosecond(), 0);
614
+ }
594
615
  }
@@ -315,4 +315,44 @@ mod tests {
315
315
  assert_eq!(do_tzoffset("est", Some(&info)), Some(-18000));
316
316
  assert_eq!(do_month("January", Some(&info)), Some(1));
317
317
  }
318
+
319
+ #[test]
320
+ fn test_parserinfo_hms() {
321
+ let info = ParserInfo::default();
322
+ assert_eq!(info.hms("hour"), Some(0));
323
+ assert_eq!(info.hms("HOURS"), Some(0));
324
+ assert_eq!(info.hms("minute"), Some(1));
325
+ assert_eq!(info.hms("s"), Some(2));
326
+ assert_eq!(info.hms("xyz"), None);
327
+ }
328
+
329
+ #[test]
330
+ fn test_parserinfo_ampm() {
331
+ let info = ParserInfo::default();
332
+ assert_eq!(info.ampm("am"), Some(0));
333
+ assert_eq!(info.ampm("AM"), Some(0));
334
+ assert_eq!(info.ampm("p"), Some(1));
335
+ assert_eq!(info.ampm("PM"), Some(1));
336
+ assert_eq!(info.ampm("xyz"), None);
337
+ }
338
+
339
+ #[test]
340
+ fn test_parserinfo_pertain() {
341
+ let info = ParserInfo::default();
342
+ assert!(info.pertain("of"));
343
+ assert!(info.pertain("OF"));
344
+ assert!(!info.pertain("xyz"));
345
+ }
346
+
347
+ #[test]
348
+ fn test_dispatch_with_some_info() {
349
+ let info = ParserInfo::default();
350
+ assert!(do_jump(",", Some(&info)));
351
+ assert_eq!(do_weekday("Monday", Some(&info)), Some(0));
352
+ assert_eq!(do_month("January", Some(&info)), Some(1));
353
+ assert_eq!(do_hms("hour", Some(&info)), Some(0));
354
+ assert_eq!(do_ampm("AM", Some(&info)), Some(0));
355
+ assert!(do_pertain("of", Some(&info)));
356
+ assert!(do_utczone("UTC", Some(&info)));
357
+ }
318
358
  }
@@ -984,22 +984,6 @@ mod tests {
984
984
  assert_eq!(dt.second(), 45);
985
985
  }
986
986
 
987
- #[test]
988
- fn test_parse_month_name() {
989
- let dt = parse("January 15, 2024", false, false, None, None).unwrap();
990
- assert_eq!(dt.year(), 2024);
991
- assert_eq!(dt.month(), 1);
992
- assert_eq!(dt.day(), 15);
993
- }
994
-
995
- #[test]
996
- fn test_parse_month_abbrev() {
997
- let dt = parse("15 Jan 2024", false, false, None, None).unwrap();
998
- assert_eq!(dt.year(), 2024);
999
- assert_eq!(dt.month(), 1);
1000
- assert_eq!(dt.day(), 15);
1001
- }
1002
-
1003
987
  #[test]
1004
988
  fn test_parse_us_format() {
1005
989
  // MM/DD/YYYY (default, dayfirst=false)
@@ -1048,24 +1032,6 @@ mod tests {
1048
1032
  assert_eq!(dt.nanosecond() / 1000, 123456);
1049
1033
  }
1050
1034
 
1051
- #[test]
1052
- fn test_parse_tz_offset() {
1053
- let res = parse_to_result("2024-01-15 10:30:45+05:30", false, false, None).unwrap();
1054
- assert_eq!(res.tzoffset, Some(5 * 3600 + 30 * 60));
1055
- }
1056
-
1057
- #[test]
1058
- fn test_parse_tz_negative() {
1059
- let res = parse_to_result("2024-01-15 10:30:45-0800", false, false, None).unwrap();
1060
- assert_eq!(res.tzoffset, Some(-(8 * 3600)));
1061
- }
1062
-
1063
- #[test]
1064
- fn test_parse_weekday() {
1065
- let res = parse_to_result("Monday, January 15, 2024", false, false, None).unwrap();
1066
- assert_eq!(res.weekday, Some(0)); // Monday
1067
- }
1068
-
1069
1035
  #[test]
1070
1036
  fn test_parse_empty_string() {
1071
1037
  assert!(parse("", false, false, None, None).is_err());
@@ -1162,19 +1128,6 @@ mod tests {
1162
1128
  assert_eq!(res.tzoffset, Some(0));
1163
1129
  }
1164
1130
 
1165
- #[test]
1166
- fn test_parse_tz_offset_compact_positive() {
1167
- let res = parse_to_result("2024-01-15 10:30:00+0530", false, false, None).unwrap();
1168
- assert_eq!(res.tzoffset, Some(5 * 3600 + 30 * 60));
1169
- }
1170
-
1171
- #[test]
1172
- fn test_parse_tz_offset_zero() {
1173
- let res = parse_to_result("2024-01-15 10:30:00+0000", false, false, None).unwrap();
1174
- assert_eq!(res.tzoffset, Some(0));
1175
- assert_eq!(res.tzname, Some("UTC".into()));
1176
- }
1177
-
1178
1131
  #[test]
1179
1132
  fn test_parse_month_all_names() {
1180
1133
  let months = [
@@ -1361,13 +1314,6 @@ mod tests {
1361
1314
  assert_eq!(res.year, Some(2024));
1362
1315
  }
1363
1316
 
1364
- #[test]
1365
- fn test_parse_time_hh_only() {
1366
- // "10 hours" — number + HMS word sets hour
1367
- let res = parse_to_result("2024-01-15 10 hours", false, false, None).unwrap();
1368
- assert_eq!(res.hour, Some(10));
1369
- }
1370
-
1371
1317
  #[test]
1372
1318
  fn test_lookup_non_ascii_returns_false() {
1373
1319
  assert!(!lookup_jump("日本語"));
@@ -1578,12 +1524,6 @@ mod tests {
1578
1524
  assert_eq!(dt.year(), 2000);
1579
1525
  }
1580
1526
 
1581
- #[test]
1582
- fn test_parse_two_digit_year_99() {
1583
- let dt = parse("01/15/99", false, false, None, None).unwrap();
1584
- assert_eq!(dt.year(), 1999);
1585
- }
1586
-
1587
1527
  // ---- Leap year edge cases ----
1588
1528
 
1589
1529
  #[test]
@@ -1672,14 +1612,6 @@ mod tests {
1672
1612
  assert_eq!(dt.second(), 0);
1673
1613
  }
1674
1614
 
1675
- #[test]
1676
- fn test_parse_end_of_day_235959() {
1677
- let dt = parse("2024-01-15 23:59:59", false, false, None, None).unwrap();
1678
- assert_eq!(dt.hour(), 23);
1679
- assert_eq!(dt.minute(), 59);
1680
- assert_eq!(dt.second(), 59);
1681
- }
1682
-
1683
1615
  // ---- Fractional seconds precision ----
1684
1616
 
1685
1617
  #[test]
@@ -1688,20 +1620,6 @@ mod tests {
1688
1620
  assert_eq!(dt.nanosecond() / 1000, 100_000);
1689
1621
  }
1690
1622
 
1691
- #[test]
1692
- fn test_parse_fractional_6_digits() {
1693
- let dt = parse("2024-01-15 10:30:45.123456", false, false, None, None).unwrap();
1694
- assert_eq!(dt.nanosecond() / 1000, 123_456);
1695
- }
1696
-
1697
- // ---- ParseResult field_count ----
1698
-
1699
- #[test]
1700
- fn test_parse_to_result_minimal() {
1701
- let res = parse_to_result("2024", false, false, None).unwrap();
1702
- assert!(res.year.is_some());
1703
- }
1704
-
1705
1623
  // ---- Only time ----
1706
1624
 
1707
1625
  #[test]
@@ -1770,4 +1688,184 @@ mod tests {
1770
1688
  assert_eq!(dt.day(), 10);
1771
1689
  assert_eq!(dt.hour(), 8);
1772
1690
  }
1691
+
1692
+ // ---- Coverage: compact parsing (try_parse_compact) ----
1693
+
1694
+ #[test]
1695
+ fn test_parse_compact_yyyymmdd() {
1696
+ let dt = parse("20240315", false, false, None, None).unwrap();
1697
+ assert_eq!(dt.year(), 2024);
1698
+ assert_eq!(dt.month(), 3);
1699
+ assert_eq!(dt.day(), 15);
1700
+ }
1701
+
1702
+ #[test]
1703
+ fn test_parse_compact_yyyymmddt_hhmmss() {
1704
+ // YYYYMMDD + T separator + HHMMSS (6-digit time after date)
1705
+ let dt = parse("20240115T103045", false, false, None, None).unwrap();
1706
+ assert_eq!(dt.year(), 2024);
1707
+ assert_eq!(dt.month(), 1);
1708
+ assert_eq!(dt.day(), 15);
1709
+ assert_eq!(dt.hour(), 10);
1710
+ assert_eq!(dt.minute(), 30);
1711
+ assert_eq!(dt.second(), 45);
1712
+ }
1713
+
1714
+ #[test]
1715
+ fn test_parse_compact_yymmdd() {
1716
+ let default = NaiveDate::from_ymd_opt(2024, 1, 1)
1717
+ .unwrap()
1718
+ .and_hms_opt(0, 0, 0)
1719
+ .unwrap();
1720
+ let dt = parse("240315", false, false, Some(default), None).unwrap();
1721
+ assert_eq!(dt.year(), 2024);
1722
+ assert_eq!(dt.month(), 3);
1723
+ assert_eq!(dt.day(), 15);
1724
+ }
1725
+
1726
+ #[test]
1727
+ fn test_parse_compact_yyyymm() {
1728
+ // "202403" → YYMMDD (20/24/03) fails because month=24 > 12
1729
+ // → YYYYMM fallback: year=2024, month=03
1730
+ let dt = parse("202403", false, false, None, None).unwrap();
1731
+ assert_eq!(dt.year(), 2024);
1732
+ assert_eq!(dt.month(), 3);
1733
+ }
1734
+
1735
+ #[test]
1736
+ fn test_parse_compact_hhmmss_after_date() {
1737
+ // Date then T then HHMMSS
1738
+ let dt = parse("2024-03-15T103045", false, false, None, None).unwrap();
1739
+ assert_eq!(dt.hour(), 10);
1740
+ assert_eq!(dt.minute(), 30);
1741
+ assert_eq!(dt.second(), 45);
1742
+ }
1743
+
1744
+ #[test]
1745
+ fn test_parse_compact_yyyymmddhh() {
1746
+ let dt = parse("2024031510", false, false, None, None).unwrap();
1747
+ assert_eq!(dt.year(), 2024);
1748
+ assert_eq!(dt.month(), 3);
1749
+ assert_eq!(dt.day(), 15);
1750
+ assert_eq!(dt.hour(), 10);
1751
+ }
1752
+
1753
+ #[test]
1754
+ fn test_parse_compact_yyyymmddhh_with_minutes() {
1755
+ let dt = parse("2024031510:30", false, false, None, None).unwrap();
1756
+ assert_eq!(dt.year(), 2024);
1757
+ assert_eq!(dt.month(), 3);
1758
+ assert_eq!(dt.day(), 15);
1759
+ assert_eq!(dt.hour(), 10);
1760
+ assert_eq!(dt.minute(), 30);
1761
+ }
1762
+
1763
+ #[test]
1764
+ fn test_parse_compact_yyyymmddhh_with_seconds() {
1765
+ let dt = parse("2024031510:30:45", false, false, None, None).unwrap();
1766
+ assert_eq!(dt.year(), 2024);
1767
+ assert_eq!(dt.month(), 3);
1768
+ assert_eq!(dt.day(), 15);
1769
+ assert_eq!(dt.hour(), 10);
1770
+ assert_eq!(dt.minute(), 30);
1771
+ assert_eq!(dt.second(), 45);
1772
+ }
1773
+
1774
+ #[test]
1775
+ fn test_parse_compact_yyyymmddhh_with_decimal_seconds() {
1776
+ let dt = parse("2024031510:30:45.5", false, false, None, None).unwrap();
1777
+ assert_eq!(dt.hour(), 10);
1778
+ assert_eq!(dt.minute(), 30);
1779
+ assert_eq!(dt.second(), 45);
1780
+ }
1781
+
1782
+ // ---- Coverage: dot-separated date (try_parse_dot_date) ----
1783
+
1784
+ #[test]
1785
+ fn test_parse_dot_separated_date() {
1786
+ let dt = parse("2003.09.25", false, false, None, None).unwrap();
1787
+ assert_eq!(dt.year(), 2003);
1788
+ assert_eq!(dt.month(), 9);
1789
+ assert_eq!(dt.day(), 25);
1790
+ }
1791
+
1792
+ #[test]
1793
+ fn test_parse_dot_separated_date_dmy() {
1794
+ let dt = parse("25.09.2003", true, false, None, None).unwrap();
1795
+ assert_eq!(dt.year(), 2003);
1796
+ assert_eq!(dt.month(), 9);
1797
+ assert_eq!(dt.day(), 25);
1798
+ }
1799
+
1800
+ // ---- Coverage: timezone parsing ----
1801
+
1802
+ #[test]
1803
+ fn test_parse_tz_offset_single_token() {
1804
+ let res = parse_to_result("2024-01-15 10:30:45 +0500", false, false, None).unwrap();
1805
+ assert_eq!(res.tzoffset, Some(18000));
1806
+ }
1807
+
1808
+ #[test]
1809
+ fn test_parse_tz_name_with_offset() {
1810
+ let res = parse_to_result("2024-01-15 10:30:45 EST -0500", false, false, None).unwrap();
1811
+ assert_eq!(res.tzname.as_deref(), Some("EST"));
1812
+ assert_eq!(res.tzoffset, Some(-18000));
1813
+ }
1814
+
1815
+ #[test]
1816
+ fn test_parse_tz_with_parserinfo() {
1817
+ let mut info = ParserInfo::default();
1818
+ info.tzoffset.insert("est".into(), -18000);
1819
+ let res = parse_to_result("2024-01-15 10:30:45 EST", false, false, Some(&info)).unwrap();
1820
+ assert_eq!(res.tzname.as_deref(), Some("EST"));
1821
+ assert_eq!(res.tzoffset, Some(-18000));
1822
+ }
1823
+
1824
+ // ---- Coverage: pertain word ----
1825
+
1826
+ #[test]
1827
+ fn test_parse_pertain_of() {
1828
+ let dt = parse("15 of January 2024", false, false, None, None).unwrap();
1829
+ assert_eq!(dt.month(), 1);
1830
+ assert_eq!(dt.day(), 15);
1831
+ }
1832
+
1833
+ // ---- Coverage: HMS assign with microseconds ----
1834
+
1835
+ #[test]
1836
+ fn test_parse_hms_label_no_space() {
1837
+ // "10h30m45s" — HMS labels immediately after numbers
1838
+ let res = parse_to_result("10h30m45s", false, false, None).unwrap();
1839
+ assert_eq!(res.hour, Some(10));
1840
+ assert_eq!(res.minute, Some(30));
1841
+ assert_eq!(res.second, Some(45));
1842
+ }
1843
+
1844
+ // ---- Coverage: convertyear edge cases ----
1845
+
1846
+ #[test]
1847
+ fn test_parse_two_digit_year_old() {
1848
+ // Two-digit year far in the past gets + 100
1849
+ let default = NaiveDate::from_ymd_opt(2024, 1, 1)
1850
+ .unwrap()
1851
+ .and_hms_opt(0, 0, 0)
1852
+ .unwrap();
1853
+ let dt = parse("01/15/70", false, false, Some(default), None).unwrap();
1854
+ // 70 should map to 1970 (2070 - 100 = 1970... or 2070 if < now+50)
1855
+ assert!(dt.year() == 1970 || dt.year() == 2070);
1856
+ }
1857
+
1858
+ // ---- Coverage: fast_parse_int / fast_parse_decimal edge cases ----
1859
+
1860
+ #[test]
1861
+ fn test_fast_parse_int_empty() {
1862
+ assert_eq!(fast_parse_int(""), None);
1863
+ }
1864
+
1865
+ #[test]
1866
+ fn test_fast_parse_decimal_empty_frac() {
1867
+ // "10." — dot at end, empty frac part → (10, 0)
1868
+ assert_eq!(fast_parse_decimal("10."), Some((10, 0)));
1869
+ }
1870
+
1773
1871
  }
@@ -1769,4 +1769,156 @@ mod tests {
1769
1769
  set.insert(rd(0, 0, 1));
1770
1770
  assert_eq!(set.len(), 2);
1771
1771
  }
1772
+
1773
+ // ---- Coverage: Display impl for absolute fields ----
1774
+
1775
+ #[test]
1776
+ fn test_display_with_absolute_fields() {
1777
+ let delta = RelativeDelta::builder()
1778
+ .year(2024)
1779
+ .month(3)
1780
+ .day(15)
1781
+ .hour(10)
1782
+ .minute(30)
1783
+ .second(45)
1784
+ .microsecond(123456)
1785
+ .build()
1786
+ .unwrap();
1787
+ let s = delta.to_string();
1788
+ assert!(s.contains("year=2024"));
1789
+ assert!(s.contains("month=3"));
1790
+ assert!(s.contains("day=15"));
1791
+ assert!(s.contains("hour=10"));
1792
+ assert!(s.contains("minute=30"));
1793
+ assert!(s.contains("second=45"));
1794
+ assert!(s.contains("microsecond=123456"));
1795
+ }
1796
+
1797
+ #[test]
1798
+ fn test_display_with_weekday() {
1799
+ use crate::common::MO;
1800
+ let delta = RelativeDelta::builder()
1801
+ .weekday(MO)
1802
+ .build()
1803
+ .unwrap();
1804
+ let s = delta.to_string();
1805
+ assert!(s.contains("weekday=MO"));
1806
+ }
1807
+
1808
+ #[test]
1809
+ fn test_display_relative_parts() {
1810
+ let delta = rd(1, 2, 3);
1811
+ let s = delta.to_string();
1812
+ assert!(s.starts_with("relativedelta("));
1813
+ assert!(s.contains("years=+1"));
1814
+ assert!(s.contains("months=+2"));
1815
+ assert!(s.contains("days=+3"));
1816
+ assert!(s.ends_with(')'));
1817
+ }
1818
+
1819
+ // ---- Coverage: set_weeks ----
1820
+
1821
+ #[test]
1822
+ fn test_set_weeks() {
1823
+ let mut delta = RelativeDelta::builder()
1824
+ .days(10)
1825
+ .build()
1826
+ .unwrap();
1827
+ assert_eq!(delta.weeks(), 1);
1828
+ delta.set_weeks(3);
1829
+ assert_eq!(delta.days(), 24); // 3*7 + 3 = 24
1830
+ assert_eq!(delta.weeks(), 3);
1831
+ }
1832
+
1833
+ // ---- Coverage: leapdays with march+ in leap year ----
1834
+
1835
+ #[test]
1836
+ fn test_leapdays_in_leap_year() {
1837
+ let delta = RelativeDelta::builder()
1838
+ .leapdays(1)
1839
+ .build()
1840
+ .unwrap();
1841
+ let base = NaiveDate::from_ymd_opt(2024, 3, 1).unwrap(); // Leap year, March
1842
+ let result = delta.add_to_naive_date(base);
1843
+ // March in a leap year: leapdays should be added
1844
+ assert_eq!(result, NaiveDate::from_ymd_opt(2024, 3, 2).unwrap());
1845
+ }
1846
+
1847
+ // ---- Coverage: weekday_eq / normalize_n ----
1848
+
1849
+ #[test]
1850
+ fn test_weekday_equality_in_relativedelta() {
1851
+ use crate::common::{MO, TU};
1852
+ let a = RelativeDelta::builder()
1853
+ .weekday(MO)
1854
+ .build()
1855
+ .unwrap();
1856
+ let b = RelativeDelta::builder()
1857
+ .weekday(MO)
1858
+ .build()
1859
+ .unwrap();
1860
+ assert_eq!(a, b);
1861
+
1862
+ let c = RelativeDelta::builder()
1863
+ .weekday(TU)
1864
+ .build()
1865
+ .unwrap();
1866
+ assert_ne!(a, c);
1867
+
1868
+ let d = RelativeDelta::builder().build().unwrap();
1869
+ assert_ne!(a, d);
1870
+ }
1871
+
1872
+ #[test]
1873
+ fn test_hash_with_weekday() {
1874
+ use crate::common::MO;
1875
+ use std::collections::hash_map::DefaultHasher;
1876
+
1877
+ let a = RelativeDelta::builder()
1878
+ .weekday(MO)
1879
+ .build()
1880
+ .unwrap();
1881
+ let b = RelativeDelta::builder()
1882
+ .weekday(MO)
1883
+ .build()
1884
+ .unwrap();
1885
+
1886
+ let hash_a = {
1887
+ let mut h = DefaultHasher::new();
1888
+ a.hash(&mut h);
1889
+ h.finish()
1890
+ };
1891
+ let hash_b = {
1892
+ let mut h = DefaultHasher::new();
1893
+ b.hash(&mut h);
1894
+ h.finish()
1895
+ };
1896
+ assert_eq!(hash_a, hash_b);
1897
+ }
1898
+
1899
+ #[test]
1900
+ fn test_hash_with_microsecond() {
1901
+ use std::collections::hash_map::DefaultHasher;
1902
+
1903
+ let a = RelativeDelta::builder()
1904
+ .microsecond(500)
1905
+ .build()
1906
+ .unwrap();
1907
+ let b = RelativeDelta::builder()
1908
+ .microsecond(500)
1909
+ .build()
1910
+ .unwrap();
1911
+
1912
+ let hash_a = {
1913
+ let mut h = DefaultHasher::new();
1914
+ a.hash(&mut h);
1915
+ h.finish()
1916
+ };
1917
+ let hash_b = {
1918
+ let mut h = DefaultHasher::new();
1919
+ b.hash(&mut h);
1920
+ h.finish()
1921
+ };
1922
+ assert_eq!(hash_a, hash_b);
1923
+ }
1772
1924
  }
@@ -1107,4 +1107,29 @@ mod tests {
1107
1107
  );
1108
1108
  assert!(err.is_err());
1109
1109
  }
1110
+
1111
+ // ---- Coverage: error paths in parse ----
1112
+
1113
+ #[test]
1114
+ fn test_rrulestr_invalid_param_without_equals() {
1115
+ // "FREQ" without "=" should error
1116
+ let result = rrulestr("FREQ", None, false, false, false);
1117
+ assert!(result.is_err());
1118
+ }
1119
+
1120
+ #[test]
1121
+ fn test_rrulestr_unknown_param_name() {
1122
+ // "XRULE:FREQ=DAILY" — unknown type prefix
1123
+ let result = rrulestr("XRULE:FREQ=DAILY;COUNT=3", None, false, false, true);
1124
+ assert!(result.is_err());
1125
+ }
1126
+
1127
+ #[test]
1128
+ fn test_rrulestr_line_unfold() {
1129
+ // Test RFC line unfolding (continuation lines starting with space)
1130
+ let input = "DTSTART:20200101T000000\nRRULE:FREQ=DAILY;\n COUNT=3";
1131
+ let result = rrulestr(input, None, false, false, true);
1132
+ assert!(result.is_ok());
1133
+ assert_eq!(result.unwrap().all().len(), 3);
1134
+ }
1110
1135
  }
@@ -645,4 +645,42 @@ mod tests {
645
645
  ]
646
646
  );
647
647
  }
648
+
649
+ // ---- Coverage: Default impl ----
650
+
651
+ #[test]
652
+ fn test_rruleset_default() {
653
+ let rset: RRuleSet = Default::default();
654
+ assert!(rset.is_finite()); // empty set is finite
655
+ }
656
+
657
+ // ---- Coverage: exrule exclusion ----
658
+
659
+ #[test]
660
+ fn test_rruleset_exrule() {
661
+ let mut rset = RRuleSet::new();
662
+ // Daily for 10 days
663
+ let rule = RRuleBuilder::new(Frequency::Daily)
664
+ .dtstart(dt(2020, 1, 1, 0, 0, 0))
665
+ .count(10)
666
+ .build()
667
+ .unwrap();
668
+ rset.rrule(rule);
669
+
670
+ // Exclude every other day (interval=2)
671
+ let exrule = RRuleBuilder::new(Frequency::Daily)
672
+ .dtstart(dt(2020, 1, 2, 0, 0, 0))
673
+ .interval(2)
674
+ .count(5)
675
+ .build()
676
+ .unwrap();
677
+ rset.exrule(exrule);
678
+
679
+ let results = rset.all();
680
+ // Should exclude Jan 2, 4, 6, 8, 10
681
+ assert!(results.contains(&dt(2020, 1, 1, 0, 0, 0)));
682
+ assert!(!results.contains(&dt(2020, 1, 2, 0, 0, 0)));
683
+ assert!(results.contains(&dt(2020, 1, 3, 0, 0, 0)));
684
+ assert!(!results.contains(&dt(2020, 1, 4, 0, 0, 0)));
685
+ }
648
686
  }
@@ -3344,4 +3344,225 @@ mod tests {
3344
3344
  let result = rule.take_slice(5, 5, 1);
3345
3345
  assert!(result.is_empty());
3346
3346
  }
3347
+
3348
+ // ---- Coverage: getter methods ----
3349
+
3350
+ #[test]
3351
+ fn test_rrule_getters() {
3352
+ let rule = RRuleBuilder::new(Frequency::Monthly)
3353
+ .dtstart(dt(2020, 1, 1, 9, 0, 0))
3354
+ .interval(2)
3355
+ .count(5)
3356
+ .bymonth(vec![1, 6])
3357
+ .bysetpos(vec![1, -1])
3358
+ .byweekno(vec![1, 52])
3359
+ .byyearday(vec![1, 365])
3360
+ .byeaster(vec![0])
3361
+ .byhour(vec![9, 17])
3362
+ .byminute(vec![0, 30])
3363
+ .bysecond(vec![0])
3364
+ .build()
3365
+ .unwrap();
3366
+
3367
+ assert_eq!(rule.freq(), Frequency::Monthly);
3368
+ assert_eq!(rule.dtstart(), dt(2020, 1, 1, 9, 0, 0));
3369
+ assert_eq!(rule.interval(), 2);
3370
+ assert_eq!(rule.wkst(), 0);
3371
+ assert_eq!(rule.count(), Some(5));
3372
+ assert_eq!(rule.until(), None);
3373
+ assert!(rule.bysetpos().is_some());
3374
+ assert!(rule.bymonth().is_some());
3375
+ assert!(rule.byyearday().is_some());
3376
+ assert!(rule.byeaster().is_some());
3377
+ assert!(rule.byweekno().is_some());
3378
+ assert!(rule.byhour().is_some());
3379
+ assert!(rule.byminute().is_some());
3380
+ assert!(rule.bysecond().is_some());
3381
+ }
3382
+
3383
+ #[test]
3384
+ fn test_rrule_bymonthday_and_bynmonthday() {
3385
+ let rule = RRuleBuilder::new(Frequency::Monthly)
3386
+ .dtstart(dt(2020, 1, 15, 0, 0, 0))
3387
+ .count(3)
3388
+ .build()
3389
+ .unwrap();
3390
+ // Default bymonthday should be dtstart day
3391
+ assert!(!rule.bymonthday().is_empty());
3392
+ assert!(rule.bynmonthday().is_empty());
3393
+ }
3394
+
3395
+ #[test]
3396
+ fn test_rrule_byweekday_getter() {
3397
+ use crate::common::{MO, FR};
3398
+ let rule = RRuleBuilder::new(Frequency::Weekly)
3399
+ .dtstart(dt(2020, 1, 1, 0, 0, 0))
3400
+ .count(5)
3401
+ .byweekday(vec![MO, FR])
3402
+ .build()
3403
+ .unwrap();
3404
+ assert!(rule.byweekday().is_some());
3405
+ }
3406
+
3407
+ #[test]
3408
+ fn test_rrule_bynweekday_getter() {
3409
+ use crate::common::MO;
3410
+ let rule = RRuleBuilder::new(Frequency::Monthly)
3411
+ .dtstart(dt(2020, 1, 1, 0, 0, 0))
3412
+ .count(3)
3413
+ .byweekday(vec![MO.with_n(Some(2))])
3414
+ .build()
3415
+ .unwrap();
3416
+ assert!(rule.bynweekday().is_some());
3417
+ }
3418
+
3419
+ // ---- Coverage: is_empty, all() panic ----
3420
+
3421
+ #[test]
3422
+ fn test_recurrence_is_empty() {
3423
+ // Rule that produces no results (month 13 doesn't exist)
3424
+ let rule = RRuleBuilder::new(Frequency::Daily)
3425
+ .dtstart(dt(2020, 1, 1, 0, 0, 0))
3426
+ .count(0)
3427
+ .build()
3428
+ .unwrap();
3429
+ assert!(rule.is_empty());
3430
+ }
3431
+
3432
+ #[test]
3433
+ #[should_panic(expected = "all() called on infinite")]
3434
+ fn test_all_panics_on_infinite() {
3435
+ let rule = RRuleBuilder::new(Frequency::Daily)
3436
+ .dtstart(dt(2020, 1, 1, 0, 0, 0))
3437
+ .build()
3438
+ .unwrap();
3439
+ let _ = rule.all();
3440
+ }
3441
+
3442
+ // ---- Coverage: len() ----
3443
+
3444
+ #[test]
3445
+ fn test_recurrence_len_finite_and_infinite() {
3446
+ let rule = RRuleBuilder::new(Frequency::Daily)
3447
+ .dtstart(dt(2020, 1, 1, 0, 0, 0))
3448
+ .count(5)
3449
+ .build()
3450
+ .unwrap();
3451
+ assert_eq!(rule.len(), Some(5));
3452
+
3453
+ let infinite = RRuleBuilder::new(Frequency::Daily)
3454
+ .dtstart(dt(2020, 1, 1, 0, 0, 0))
3455
+ .build()
3456
+ .unwrap();
3457
+ assert_eq!(infinite.len(), None);
3458
+ }
3459
+
3460
+ // ---- Coverage: Arc<RRule> iter ----
3461
+
3462
+ #[test]
3463
+ fn test_arc_rrule_iter() {
3464
+ let rule = Arc::new(
3465
+ RRuleBuilder::new(Frequency::Daily)
3466
+ .dtstart(dt(2020, 1, 1, 0, 0, 0))
3467
+ .count(3)
3468
+ .build()
3469
+ .unwrap(),
3470
+ );
3471
+ let results: Vec<_> = rule.iter().collect();
3472
+ assert_eq!(results.len(), 3);
3473
+ assert!(rule.is_finite());
3474
+ }
3475
+
3476
+ // ---- Coverage: Display impl for RRULE ----
3477
+
3478
+ #[test]
3479
+ fn test_rrule_display_with_bysetpos() {
3480
+ let rule = RRuleBuilder::new(Frequency::Monthly)
3481
+ .dtstart(dt(2020, 1, 1, 0, 0, 0))
3482
+ .count(3)
3483
+ .bysetpos(vec![1, -1])
3484
+ .bymonth(vec![1, 6])
3485
+ .byyearday(vec![1, 100])
3486
+ .byweekno(vec![1, 52])
3487
+ .byhour(vec![9, 17])
3488
+ .byminute(vec![0, 30])
3489
+ .bysecond(vec![0])
3490
+ .byeaster(vec![0, -2])
3491
+ .build()
3492
+ .unwrap();
3493
+ let s = rule.to_string();
3494
+ assert!(s.contains("BYSETPOS=1,-1"));
3495
+ assert!(s.contains("BYMONTH="));
3496
+ assert!(s.contains("BYYEARDAY="));
3497
+ assert!(s.contains("BYWEEKNO="));
3498
+ assert!(s.contains("BYHOUR="));
3499
+ assert!(s.contains("BYMINUTE="));
3500
+ assert!(s.contains("BYSECOND="));
3501
+ assert!(s.contains("BYEASTER="));
3502
+ }
3503
+
3504
+ #[test]
3505
+ fn test_rrule_display_with_bymonthday() {
3506
+ let rule = RRuleBuilder::new(Frequency::Monthly)
3507
+ .dtstart(dt(2020, 1, 1, 0, 0, 0))
3508
+ .count(3)
3509
+ .bymonthday(vec![1, 15])
3510
+ .build()
3511
+ .unwrap();
3512
+ let s = rule.to_string();
3513
+ assert!(s.contains("BYMONTHDAY="));
3514
+ }
3515
+
3516
+ // ---- Coverage: construct_byset empty error ----
3517
+
3518
+ #[test]
3519
+ fn test_construct_byset_empty() {
3520
+ // interval=2, start=0, byxxx=[1] → gcd(2,24)=2, 1%2=1≠0 → empty
3521
+ let result = construct_byset(0, &[1], 24, 2);
3522
+ assert!(result.is_err());
3523
+ }
3524
+
3525
+ // ---- Coverage: mod_distance returns None ----
3526
+
3527
+ #[test]
3528
+ fn test_mod_distance_none() {
3529
+ // No matching value within base iterations
3530
+ let result = mod_distance(0, &[], 60, 1);
3531
+ assert_eq!(result, None);
3532
+ }
3533
+
3534
+ // ---- Coverage: days_in_month invalid month ----
3535
+
3536
+ #[test]
3537
+ fn test_days_in_month_invalid() {
3538
+ assert_eq!(days_in_month(2024, 0), 0);
3539
+ assert_eq!(days_in_month(2024, 13), 0);
3540
+ }
3541
+
3542
+ // ---- Coverage: invalid wkst ----
3543
+
3544
+ #[test]
3545
+ fn test_builder_invalid_wkst_7() {
3546
+ let result = RRuleBuilder::new(Frequency::Daily)
3547
+ .dtstart(dt(2020, 1, 1, 0, 0, 0))
3548
+ .wkst(7)
3549
+ .build();
3550
+ assert!(result.is_err());
3551
+ }
3552
+
3553
+ // ---- Coverage: freq > Monthly with nth weekday (flattened to plain) ----
3554
+
3555
+ #[test]
3556
+ fn test_weekly_nth_weekday_flattened() {
3557
+ use crate::common::MO;
3558
+ // nth weekday with freq > Monthly → pushed to plain weekday
3559
+ let rule = RRuleBuilder::new(Frequency::Weekly)
3560
+ .dtstart(dt(2020, 1, 6, 0, 0, 0)) // Monday
3561
+ .count(3)
3562
+ .byweekday(vec![MO.with_n(Some(2))])
3563
+ .build()
3564
+ .unwrap();
3565
+ let results = rule.all();
3566
+ assert_eq!(results.len(), 3);
3567
+ }
3347
3568
  }
@@ -230,4 +230,16 @@ mod tests {
230
230
  set.insert(a);
231
231
  assert!(set.contains(&b));
232
232
  }
233
+
234
+ #[test]
235
+ fn test_display_name_method() {
236
+ let named = TzOffset::new(Some("EST"), -5 * 3600);
237
+ assert_eq!(named.display_name(), "EST");
238
+
239
+ let unnamed = TzOffset::new(None, 5 * 3600 + 1800);
240
+ assert_eq!(unnamed.display_name(), "UTC+05:30");
241
+
242
+ let utc = TzOffset::new(None, 0);
243
+ assert_eq!(utc.display_name(), "UTC");
244
+ }
233
245
  }
@@ -315,20 +315,6 @@ mod tests {
315
315
  // gettz()
316
316
  // -----------------------------------------------------------------------
317
317
 
318
- #[test]
319
- fn test_gettz_utc() {
320
- let tz = gettz(Some("UTC")).unwrap();
321
- let d = dt(2024, 1, 1, 0, 0, 0);
322
- assert_eq!(tz.utcoffset(d, false), 0);
323
- assert_eq!(tz.tzname(d, false), "UTC");
324
- }
325
-
326
- #[test]
327
- fn test_gettz_gmt() {
328
- let tz = gettz(Some("GMT")).unwrap();
329
- assert_eq!(tz.utcoffset(dt(2024, 1, 1, 0, 0, 0), false), 0);
330
- }
331
-
332
318
  #[test]
333
319
  fn test_gettz_iana_name() {
334
320
  let tz = gettz(Some("America/New_York")).unwrap();
@@ -589,4 +575,42 @@ mod tests {
589
575
  // July is WINTER: AEST (UTC+10)
590
576
  assert_eq!(tz.utcoffset(dt(2024, 7, 15, 12, 0, 0), false), 10 * 3600);
591
577
  }
578
+
579
+ // ---- Coverage: TimeZone dispatch for Local variant ----
580
+
581
+ #[test]
582
+ fn test_timezone_local_dst() {
583
+ let tz = gettz(None).unwrap(); // local timezone
584
+ let d = dt(2024, 7, 15, 12, 0, 0);
585
+ // Just verify it doesn't panic
586
+ let _ = tz.dst(d, false);
587
+ }
588
+
589
+ #[test]
590
+ fn test_timezone_local_tzname() {
591
+ let tz = gettz(None).unwrap();
592
+ let d = dt(2024, 1, 15, 12, 0, 0);
593
+ let name = tz.tzname(d, false);
594
+ assert!(!name.is_empty());
595
+ }
596
+
597
+ #[test]
598
+ fn test_timezone_local_is_ambiguous() {
599
+ let tz = gettz(None).unwrap();
600
+ let d = dt(2024, 6, 15, 12, 0, 0);
601
+ // Just verify it doesn't panic; midday is never ambiguous
602
+ let _ = tz.is_ambiguous(d);
603
+ }
604
+
605
+ #[test]
606
+ fn test_timezone_local_fromutc() {
607
+ let tz = gettz(None).unwrap();
608
+ let d = dt(2024, 1, 15, 12, 0, 0);
609
+ let wall = tz.fromutc(d);
610
+ // fromutc should shift by the local offset
611
+ let offset = tz.utcoffset(wall, false);
612
+ // The difference should match the offset
613
+ let diff = (wall - d).num_seconds() as i32;
614
+ assert_eq!(diff, offset);
615
+ }
592
616
  }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "dateutil-py"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  edition = "2021"
5
5
  license = "MIT"
6
6
  description = "PyO3 bindings for the dateutil-core crate"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-dateutil-rs"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "A Rust-backed port of python-dateutil"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1,15 @@
1
+ """dateutil_rs.easter - Easter date calculation."""
2
+
3
+ from dateutil_rs._native import (
4
+ EASTER_JULIAN,
5
+ EASTER_ORTHODOX,
6
+ EASTER_WESTERN,
7
+ easter,
8
+ )
9
+
10
+ __all__ = [
11
+ "EASTER_JULIAN",
12
+ "EASTER_ORTHODOX",
13
+ "EASTER_WESTERN",
14
+ "easter",
15
+ ]
@@ -0,0 +1,5 @@
1
+ """dateutil_rs.relativedelta - Relative date/time arithmetic."""
2
+
3
+ from dateutil_rs._native import relativedelta
4
+
5
+ __all__ = ["relativedelta"]
@@ -0,0 +1,27 @@
1
+ """dateutil_rs.rrule - RFC 5545 recurrence rules."""
2
+
3
+ from dateutil_rs._native import (
4
+ DAILY,
5
+ HOURLY,
6
+ MINUTELY,
7
+ MONTHLY,
8
+ SECONDLY,
9
+ WEEKLY,
10
+ YEARLY,
11
+ rrule,
12
+ rruleset,
13
+ rrulestr,
14
+ )
15
+
16
+ __all__ = [
17
+ "DAILY",
18
+ "HOURLY",
19
+ "MINUTELY",
20
+ "MONTHLY",
21
+ "SECONDLY",
22
+ "WEEKLY",
23
+ "YEARLY",
24
+ "rrule",
25
+ "rruleset",
26
+ "rrulestr",
27
+ ]
@@ -0,0 +1,23 @@
1
+ """dateutil_rs.tz - Timezone types and utilities."""
2
+
3
+ from dateutil_rs._native import (
4
+ datetime_ambiguous,
5
+ datetime_exists,
6
+ gettz,
7
+ resolve_imaginary,
8
+ tzfile,
9
+ tzlocal,
10
+ tzoffset,
11
+ tzutc,
12
+ )
13
+
14
+ __all__ = [
15
+ "datetime_ambiguous",
16
+ "datetime_exists",
17
+ "gettz",
18
+ "resolve_imaginary",
19
+ "tzfile",
20
+ "tzlocal",
21
+ "tzoffset",
22
+ "tzutc",
23
+ ]