python-dateutil-rs 0.0.12__tar.gz → 0.0.14__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 (78) hide show
  1. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/Cargo.lock +16 -15
  2. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/PKG-INFO +13 -6
  3. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/README.md +12 -5
  4. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-core/Cargo.toml +1 -0
  5. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-core/benches/benchmarks.rs +9 -9
  6. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-core/src/error.rs +16 -0
  7. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-core/src/lib.rs +1 -0
  8. python_dateutil_rs-0.0.14/crates/dateutil-core/src/parser/parserinfo.rs +318 -0
  9. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-core/src/parser.rs +165 -131
  10. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-core/src/rrule/iter.rs +12 -5
  11. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-core/src/rrule/set.rs +17 -15
  12. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-core/src/rrule.rs +29 -0
  13. python_dateutil_rs-0.0.14/crates/dateutil-core/src/tz/file.rs +1212 -0
  14. python_dateutil_rs-0.0.14/crates/dateutil-core/src/tz/local.rs +198 -0
  15. python_dateutil_rs-0.0.14/crates/dateutil-core/src/tz/offset.rs +233 -0
  16. python_dateutil_rs-0.0.14/crates/dateutil-core/src/tz/utc.rs +77 -0
  17. python_dateutil_rs-0.0.14/crates/dateutil-core/src/tz.rs +592 -0
  18. python_dateutil_rs-0.0.14/crates/dateutil-py/src/py/conv.rs +176 -0
  19. python_dateutil_rs-0.0.14/crates/dateutil-py/src/py/parser.rs +335 -0
  20. python_dateutil_rs-0.0.14/crates/dateutil-py/src/py/relativedelta.rs +330 -0
  21. python_dateutil_rs-0.0.14/crates/dateutil-py/src/py/tz.rs +371 -0
  22. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-py/src/py.rs +3 -0
  23. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/Cargo.toml +1 -1
  24. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/README.md +12 -5
  25. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/pyproject.toml +1 -1
  26. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/v1/__init__.py +20 -1
  27. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/v1/_native.pyi +123 -8
  28. python_dateutil_rs-0.0.14/python/dateutil_rs/v1/parser.py +18 -0
  29. python_dateutil_rs-0.0.14/python/dateutil_rs/v1/tz.py +23 -0
  30. python_dateutil_rs-0.0.12/crates/dateutil-py/src/py/parser.rs +0 -63
  31. python_dateutil_rs-0.0.12/crates/dateutil-py/src/py/relativedelta.rs +0 -206
  32. python_dateutil_rs-0.0.12/python/dateutil_rs/v1/parser.py +0 -5
  33. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/Cargo.toml +0 -0
  34. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/LICENSE +0 -0
  35. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-core/CLAUDE.md +0 -0
  36. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-core/src/common.rs +0 -0
  37. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-core/src/easter.rs +0 -0
  38. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-core/src/parser/isoparser.rs +0 -0
  39. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-core/src/parser/tokenizer.rs +0 -0
  40. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-core/src/relativedelta.rs +0 -0
  41. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-core/src/rrule/parse.rs +0 -0
  42. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-py/Cargo.toml +0 -0
  43. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-py/src/lib.rs +0 -0
  44. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-py/src/py/common.rs +0 -0
  45. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-py/src/py/easter.rs +0 -0
  46. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-py/src/py/rrule.rs +0 -0
  47. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/LICENSE +0 -0
  48. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/benches/benchmarks.rs +0 -0
  49. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/src/common.rs +0 -0
  50. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/src/easter.rs +0 -0
  51. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/src/lib.rs +0 -0
  52. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/src/parser/isoparser.rs +0 -0
  53. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/src/parser.rs +0 -0
  54. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/src/relativedelta.rs +0 -0
  55. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/src/rrule/iter.rs +0 -0
  56. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/src/rrule.rs +0 -0
  57. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/src/tz/file.rs +0 -0
  58. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/src/tz/local.rs +0 -0
  59. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/src/tz/offset.rs +0 -0
  60. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/src/tz/range.rs +0 -0
  61. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/src/tz/utc.rs +0 -0
  62. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/src/tz.rs +0 -0
  63. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/crates/dateutil-rs/src/utils.rs +0 -0
  64. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/__init__.py +0 -0
  65. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/_native.pyi +0 -0
  66. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/common.py +0 -0
  67. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/easter.py +0 -0
  68. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/parser.py +0 -0
  69. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/py.typed +0 -0
  70. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/relativedelta.py +0 -0
  71. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/rrule.py +0 -0
  72. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/tz.py +0 -0
  73. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/utils.py +0 -0
  74. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/v1/common.py +0 -0
  75. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/v1/easter.py +0 -0
  76. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/v1/py.typed +0 -0
  77. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/v1/relativedelta.py +0 -0
  78. {python_dateutil_rs-0.0.12 → python_dateutil_rs-0.0.14}/python/dateutil_rs/v1/rrule.py +0 -0
@@ -67,9 +67,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
67
67
 
68
68
  [[package]]
69
69
  name = "cc"
70
- version = "1.2.59"
70
+ version = "1.2.60"
71
71
  source = "registry+https://github.com/rust-lang/crates.io-index"
72
- checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283"
72
+ checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
73
73
  dependencies = [
74
74
  "find-msvc-tools",
75
75
  "shlex",
@@ -225,6 +225,7 @@ dependencies = [
225
225
  "bitflags",
226
226
  "chrono",
227
227
  "criterion",
228
+ "iana-time-zone",
228
229
  "phf",
229
230
  "smallvec",
230
231
  "thiserror",
@@ -241,7 +242,7 @@ dependencies = [
241
242
 
242
243
  [[package]]
243
244
  name = "dateutil-rs"
244
- version = "0.0.12"
245
+ version = "0.0.14"
245
246
  dependencies = [
246
247
  "chrono",
247
248
  "criterion",
@@ -326,9 +327,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
326
327
 
327
328
  [[package]]
328
329
  name = "js-sys"
329
- version = "0.3.94"
330
+ version = "0.3.95"
330
331
  source = "registry+https://github.com/rust-lang/crates.io-index"
331
- checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
332
+ checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
332
333
  dependencies = [
333
334
  "once_cell",
334
335
  "wasm-bindgen",
@@ -727,9 +728,9 @@ dependencies = [
727
728
 
728
729
  [[package]]
729
730
  name = "wasm-bindgen"
730
- version = "0.2.117"
731
+ version = "0.2.118"
731
732
  source = "registry+https://github.com/rust-lang/crates.io-index"
732
- checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
733
+ checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
733
734
  dependencies = [
734
735
  "cfg-if",
735
736
  "once_cell",
@@ -740,9 +741,9 @@ dependencies = [
740
741
 
741
742
  [[package]]
742
743
  name = "wasm-bindgen-macro"
743
- version = "0.2.117"
744
+ version = "0.2.118"
744
745
  source = "registry+https://github.com/rust-lang/crates.io-index"
745
- checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
746
+ checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
746
747
  dependencies = [
747
748
  "quote",
748
749
  "wasm-bindgen-macro-support",
@@ -750,9 +751,9 @@ dependencies = [
750
751
 
751
752
  [[package]]
752
753
  name = "wasm-bindgen-macro-support"
753
- version = "0.2.117"
754
+ version = "0.2.118"
754
755
  source = "registry+https://github.com/rust-lang/crates.io-index"
755
- checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
756
+ checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
756
757
  dependencies = [
757
758
  "bumpalo",
758
759
  "proc-macro2",
@@ -763,18 +764,18 @@ dependencies = [
763
764
 
764
765
  [[package]]
765
766
  name = "wasm-bindgen-shared"
766
- version = "0.2.117"
767
+ version = "0.2.118"
767
768
  source = "registry+https://github.com/rust-lang/crates.io-index"
768
- checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
769
+ checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
769
770
  dependencies = [
770
771
  "unicode-ident",
771
772
  ]
772
773
 
773
774
  [[package]]
774
775
  name = "web-sys"
775
- version = "0.3.94"
776
+ version = "0.3.95"
776
777
  source = "registry+https://github.com/rust-lang/crates.io-index"
777
- checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a"
778
+ checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
778
779
  dependencies = [
779
780
  "js-sys",
780
781
  "wasm-bindgen",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-dateutil-rs
3
- Version: 0.0.12
3
+ Version: 0.0.14
4
4
  Classifier: Programming Language :: Python :: 3.10
5
5
  Classifier: Programming Language :: Python :: 3.11
6
6
  Classifier: Programming Language :: Python :: 3.12
@@ -84,6 +84,7 @@ from dateutil_rs.v1.relativedelta import relativedelta
84
84
  from dateutil_rs.v1.rrule import rrule, rruleset, MONTHLY
85
85
  from dateutil_rs.v1.easter import easter
86
86
  from dateutil_rs.v1.common import MO, TU, WE, TH, FR, SA, SU
87
+ from dateutil_rs.v1.tz import gettz, tzutc
87
88
 
88
89
  # Parse date strings (zero-copy tokenizer)
89
90
  dt = parse("2024-01-15T10:30:00")
@@ -91,6 +92,10 @@ dt = parse("2024-01-15T10:30:00")
91
92
  # Recurrence rules (buffer-reusing iterator)
92
93
  monthly = rrule(MONTHLY, count=5, dtstart=dt)
93
94
  dates = monthly.all()
95
+
96
+ # Timezones
97
+ tokyo = gettz("Asia/Tokyo")
98
+ utc = tzutc()
94
99
  ```
95
100
 
96
101
  ## Development
@@ -220,11 +225,12 @@ dateutil-rs/
220
225
  │ │ ├── parser.rs # parse() + zero-copy tokenizer
221
226
  │ │ ├── parser/ # isoparser
222
227
  │ │ ├── rrule.rs # RRule + iterator
223
- │ │ └── rrule/ # set, parse (rrulestr), iter
228
+ │ │ ├── rrule/ # set, parse (rrulestr), iter
229
+ │ │ └── tz/ # tzutc, tzoffset, tzfile, tzlocal
224
230
  │ ├── dateutil-py/ # PyO3 bindings for v1 core
225
231
  │ │ └── src/
226
232
  │ │ ├── lib.rs # Module registration
227
- │ │ └── py/ # Per-module bindings
233
+ │ │ └── py/ # Per-module bindings (common, easter, parser, relativedelta, rrule, tz)
228
234
  │ └── dateutil-rs/ # v0: python-dateutil compat + unified native module
229
235
  │ └── src/
230
236
  │ ├── lib.rs # Crate root + #[pymodule] (v0 + v1)
@@ -251,7 +257,8 @@ dateutil-rs/
251
257
  │ ├── easter.py # Easter
252
258
  │ ├── parser.py # parse, isoparse
253
259
  │ ├── relativedelta.py
254
- └── rrule.py # rrule, rruleset, rrulestr
260
+ ├── rrule.py # rrule, rruleset, rrulestr
261
+ │ └── tz.py # tzutc, tzoffset, tzfile, tzlocal, gettz
255
262
  ├── tests/ # Test suite (~13k lines)
256
263
  ├── benchmarks/ # pytest-benchmark comparisons
257
264
  ├── .github/workflows/ # CI (lint + test matrix)
@@ -284,7 +291,7 @@ dateutil-rs/
284
291
  | relativedelta | ✅ | ✅ | |
285
292
  | parser | ✅ | ✅ | Zero-copy tokenizer, PHF lookups |
286
293
  | rrule | ✅ | ✅ | Bitflag filters, buffer reuse |
287
- | tz | | | Planned |
294
+ | tz | | | tzutc, tzoffset, tzfile, tzlocal, gettz |
288
295
 
289
296
  ## Roadmap
290
297
 
@@ -293,7 +300,7 @@ dateutil-rs/
293
300
  3. **~~Rust rrule~~** — Rewrite recurrence rules in Rust ✅
294
301
  4. **~~Rust tz~~** — Rewrite timezone support in Rust (with gettz cache) ✅
295
302
  5. **~~v1 optimized core~~** — common, easter, relativedelta, parser, rrule ✅
296
- 6. **v1 timezone** — Rewrite tz module for v1 core
303
+ 6. **~~v1 timezone~~** — Rewrite tz module for v1 core (tzutc, tzoffset, tzfile, tzlocal, gettz) ✅
297
304
  7. **Release** — Publish dateutil-core to crates.io and python-dateutil-rs 1.0 to PyPI
298
305
 
299
306
  ## License
@@ -62,6 +62,7 @@ from dateutil_rs.v1.relativedelta import relativedelta
62
62
  from dateutil_rs.v1.rrule import rrule, rruleset, MONTHLY
63
63
  from dateutil_rs.v1.easter import easter
64
64
  from dateutil_rs.v1.common import MO, TU, WE, TH, FR, SA, SU
65
+ from dateutil_rs.v1.tz import gettz, tzutc
65
66
 
66
67
  # Parse date strings (zero-copy tokenizer)
67
68
  dt = parse("2024-01-15T10:30:00")
@@ -69,6 +70,10 @@ dt = parse("2024-01-15T10:30:00")
69
70
  # Recurrence rules (buffer-reusing iterator)
70
71
  monthly = rrule(MONTHLY, count=5, dtstart=dt)
71
72
  dates = monthly.all()
73
+
74
+ # Timezones
75
+ tokyo = gettz("Asia/Tokyo")
76
+ utc = tzutc()
72
77
  ```
73
78
 
74
79
  ## Development
@@ -198,11 +203,12 @@ dateutil-rs/
198
203
  │ │ ├── parser.rs # parse() + zero-copy tokenizer
199
204
  │ │ ├── parser/ # isoparser
200
205
  │ │ ├── rrule.rs # RRule + iterator
201
- │ │ └── rrule/ # set, parse (rrulestr), iter
206
+ │ │ ├── rrule/ # set, parse (rrulestr), iter
207
+ │ │ └── tz/ # tzutc, tzoffset, tzfile, tzlocal
202
208
  │ ├── dateutil-py/ # PyO3 bindings for v1 core
203
209
  │ │ └── src/
204
210
  │ │ ├── lib.rs # Module registration
205
- │ │ └── py/ # Per-module bindings
211
+ │ │ └── py/ # Per-module bindings (common, easter, parser, relativedelta, rrule, tz)
206
212
  │ └── dateutil-rs/ # v0: python-dateutil compat + unified native module
207
213
  │ └── src/
208
214
  │ ├── lib.rs # Crate root + #[pymodule] (v0 + v1)
@@ -229,7 +235,8 @@ dateutil-rs/
229
235
  │ ├── easter.py # Easter
230
236
  │ ├── parser.py # parse, isoparse
231
237
  │ ├── relativedelta.py
232
- └── rrule.py # rrule, rruleset, rrulestr
238
+ ├── rrule.py # rrule, rruleset, rrulestr
239
+ │ └── tz.py # tzutc, tzoffset, tzfile, tzlocal, gettz
233
240
  ├── tests/ # Test suite (~13k lines)
234
241
  ├── benchmarks/ # pytest-benchmark comparisons
235
242
  ├── .github/workflows/ # CI (lint + test matrix)
@@ -262,7 +269,7 @@ dateutil-rs/
262
269
  | relativedelta | ✅ | ✅ | |
263
270
  | parser | ✅ | ✅ | Zero-copy tokenizer, PHF lookups |
264
271
  | rrule | ✅ | ✅ | Bitflag filters, buffer reuse |
265
- | tz | | | Planned |
272
+ | tz | | | tzutc, tzoffset, tzfile, tzlocal, gettz |
266
273
 
267
274
  ## Roadmap
268
275
 
@@ -271,7 +278,7 @@ dateutil-rs/
271
278
  3. **~~Rust rrule~~** — Rewrite recurrence rules in Rust ✅
272
279
  4. **~~Rust tz~~** — Rewrite timezone support in Rust (with gettz cache) ✅
273
280
  5. **~~v1 optimized core~~** — common, easter, relativedelta, parser, rrule ✅
274
- 6. **v1 timezone** — Rewrite tz module for v1 core
281
+ 6. **~~v1 timezone~~** — Rewrite tz module for v1 core (tzutc, tzoffset, tzfile, tzlocal, gettz) ✅
275
282
  7. **Release** — Publish dateutil-core to crates.io and python-dateutil-rs 1.0 to PyPI
276
283
 
277
284
  ## License
@@ -15,6 +15,7 @@ chrono = "0.4"
15
15
  phf = { version = "0.13", features = ["macros"] }
16
16
  smallvec = "1.15"
17
17
  thiserror = "2"
18
+ iana-time-zone = "0.1"
18
19
 
19
20
  [dev-dependencies]
20
21
  criterion = { version = "0.8", features = ["html_reports"] }
@@ -41,26 +41,26 @@ fn bench_tokenizer(c: &mut Criterion) {
41
41
  fn bench_parser(c: &mut Criterion) {
42
42
  c.bench_function("parse_iso_date", |b| {
43
43
  b.iter(|| {
44
- black_box(parser::parse(black_box("2024-01-15"), false, false, None).unwrap());
44
+ black_box(parser::parse(black_box("2024-01-15"), false, false, None, None).unwrap());
45
45
  })
46
46
  });
47
47
 
48
48
  c.bench_function("parse_datetime", |b| {
49
49
  b.iter(|| {
50
- black_box(parser::parse(black_box("2024-01-15 10:30:45"), false, false, None).unwrap());
50
+ black_box(parser::parse(black_box("2024-01-15 10:30:45"), false, false, None, None).unwrap());
51
51
  })
52
52
  });
53
53
 
54
54
  c.bench_function("parse_month_name", |b| {
55
55
  b.iter(|| {
56
- black_box(parser::parse(black_box("January 15, 2024"), false, false, None).unwrap());
56
+ black_box(parser::parse(black_box("January 15, 2024"), false, false, None, None).unwrap());
57
57
  })
58
58
  });
59
59
 
60
60
  c.bench_function("parse_complex", |b| {
61
61
  b.iter(|| {
62
62
  black_box(
63
- parser::parse(black_box("Monday, January 15, 2024 3:30:45.123456 PM UTC"), false, false, None)
63
+ parser::parse(black_box("Monday, January 15, 2024 3:30:45.123456 PM UTC"), false, false, None, None)
64
64
  .unwrap(),
65
65
  );
66
66
  })
@@ -71,7 +71,7 @@ fn bench_parser(c: &mut Criterion) {
71
71
  black_box(
72
72
  parser::parse_to_result(
73
73
  black_box("Monday, January 15, 2024 3:30:45.123456 PM EST -05:00"),
74
- false, false,
74
+ false, false, None,
75
75
  ).unwrap(),
76
76
  );
77
77
  })
@@ -80,7 +80,7 @@ fn bench_parser(c: &mut Criterion) {
80
80
  c.bench_function("parse_tz_positive_offset", |b| {
81
81
  b.iter(|| {
82
82
  black_box(
83
- parser::parse_to_result(black_box("2024-01-15 10:30:45+05:30"), false, false)
83
+ parser::parse_to_result(black_box("2024-01-15 10:30:45+05:30"), false, false, None)
84
84
  .unwrap(),
85
85
  );
86
86
  })
@@ -89,7 +89,7 @@ fn bench_parser(c: &mut Criterion) {
89
89
  c.bench_function("parse_tz_negative_offset", |b| {
90
90
  b.iter(|| {
91
91
  black_box(
92
- parser::parse_to_result(black_box("2024-01-15 10:30:45-0800"), false, false)
92
+ parser::parse_to_result(black_box("2024-01-15 10:30:45-0800"), false, false, None)
93
93
  .unwrap(),
94
94
  );
95
95
  })
@@ -97,7 +97,7 @@ fn bench_parser(c: &mut Criterion) {
97
97
 
98
98
  c.bench_function("parse_ampm", |b| {
99
99
  b.iter(|| {
100
- black_box(parser::parse(black_box("January 15, 2024 3:30 PM"), false, false, None).unwrap());
100
+ black_box(parser::parse(black_box("January 15, 2024 3:30 PM"), false, false, None, None).unwrap());
101
101
  })
102
102
  });
103
103
 
@@ -135,7 +135,7 @@ fn bench_parser_throughput(c: &mut Criterion) {
135
135
  c.bench_function("parse_throughput_8_inputs", |b| {
136
136
  b.iter(|| {
137
137
  for input in &inputs {
138
- black_box(parser::parse(black_box(input), false, false, None).unwrap());
138
+ black_box(parser::parse(black_box(input), false, false, None, None).unwrap());
139
139
  }
140
140
  })
141
141
  });
@@ -13,6 +13,8 @@ pub enum Error {
13
13
  Parse(#[from] ParseError),
14
14
  #[error("{0}")]
15
15
  RRule(#[from] RRuleError),
16
+ #[error("{0}")]
17
+ Tz(#[from] TzError),
16
18
  }
17
19
 
18
20
  #[derive(Debug, Clone, PartialEq, Eq, Error)]
@@ -65,6 +67,20 @@ pub enum RRuleError {
65
67
  ValueError(Box<str>),
66
68
  }
67
69
 
70
+ #[derive(Debug, Clone, PartialEq, Eq, Error)]
71
+ pub enum TzError {
72
+ #[error("I/O error: {0}")]
73
+ Io(Box<str>),
74
+ #[error("invalid TZif magic: expected 'TZif'")]
75
+ InvalidMagic,
76
+ #[error("invalid TZif data: {0}")]
77
+ InvalidData(Box<str>),
78
+ #[error("invalid POSIX TZ string: {0}")]
79
+ InvalidPosixTz(Box<str>),
80
+ #[error("timezone not found: {0}")]
81
+ NotFound(Box<str>),
82
+ }
83
+
68
84
  #[cfg(test)]
69
85
  mod tests {
70
86
  use super::*;
@@ -4,3 +4,4 @@ pub mod error;
4
4
  pub mod parser;
5
5
  pub mod relativedelta;
6
6
  pub mod rrule;
7
+ pub mod tz;
@@ -0,0 +1,318 @@
1
+ use std::collections::{HashMap, HashSet};
2
+
3
+ use super::{lowercase_buf, lower_str};
4
+ use super::{lookup_jump, lookup_weekday, lookup_month, lookup_hms, lookup_ampm, lookup_pertain, lookup_utczone};
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // ParserInfo — custom lookup tables for non-default locale support
8
+ // ---------------------------------------------------------------------------
9
+
10
+ /// Custom parser configuration that overrides the default PHF lookup tables.
11
+ ///
12
+ /// All string keys must be stored in **lowercase** for case-insensitive matching.
13
+ /// Use [`ParserInfo::default()`] to get the standard English tables as `HashMap`s.
14
+ pub struct ParserInfo {
15
+ /// Jump words — ignored during parsing (e.g. "at", "on", ",").
16
+ pub jump: HashSet<String>,
17
+ /// Weekday name → 0-based index (Mon=0 .. Sun=6).
18
+ pub weekdays: HashMap<String, usize>,
19
+ /// Month name → 1-based index (Jan=1 .. Dec=12).
20
+ pub months: HashMap<String, usize>,
21
+ /// HMS indicator → 0=hour, 1=minute, 2=second.
22
+ pub hms: HashMap<String, usize>,
23
+ /// AM/PM → 0=AM, 1=PM.
24
+ pub ampm: HashMap<String, usize>,
25
+ /// UTC-equivalent zone names.
26
+ pub utczone: HashSet<String>,
27
+ /// Pertain words (e.g. "of").
28
+ pub pertain: HashSet<String>,
29
+ /// Known timezone abbreviations → offset in seconds.
30
+ pub tzoffset: HashMap<String, i32>,
31
+ }
32
+
33
+ impl Default for ParserInfo {
34
+ fn default() -> Self {
35
+ let jump: HashSet<String> = [
36
+ " ", ".", ",", ";", "-", "/", "'",
37
+ "at", "on", "and", "ad", "m", "t", "of",
38
+ "st", "nd", "rd", "th",
39
+ ].into_iter().map(String::from).collect();
40
+
41
+ let mut weekdays = HashMap::new();
42
+ for (i, names) in [
43
+ &["mon", "monday"][..], &["tue", "tuesday"], &["wed", "wednesday"],
44
+ &["thu", "thursday"], &["fri", "friday"], &["sat", "saturday"],
45
+ &["sun", "sunday"],
46
+ ].iter().enumerate() {
47
+ for name in *names {
48
+ weekdays.insert(String::from(*name), i);
49
+ }
50
+ }
51
+
52
+ let mut months = HashMap::new();
53
+ for (i, names) in [
54
+ &["jan", "january"][..], &["feb", "february"], &["mar", "march"],
55
+ &["apr", "april"], &["may"][..], &["jun", "june"],
56
+ &["jul", "july"], &["aug", "august"],
57
+ &["sep", "sept", "september"], &["oct", "october"],
58
+ &["nov", "november"], &["dec", "december"],
59
+ ].iter().enumerate() {
60
+ for name in *names {
61
+ months.insert(String::from(*name), i + 1);
62
+ }
63
+ }
64
+
65
+ let mut hms = HashMap::new();
66
+ for (i, names) in [
67
+ &["h", "hour", "hours"][..],
68
+ &["m", "minute", "minutes"],
69
+ &["s", "second", "seconds"],
70
+ ].iter().enumerate() {
71
+ for name in *names {
72
+ hms.insert(String::from(*name), i);
73
+ }
74
+ }
75
+
76
+ let mut ampm = HashMap::new();
77
+ for (i, names) in [&["am", "a"][..], &["pm", "p"]].iter().enumerate() {
78
+ for name in *names {
79
+ ampm.insert(String::from(*name), i);
80
+ }
81
+ }
82
+
83
+ let utczone: HashSet<String> =
84
+ ["utc", "gmt", "z"].into_iter().map(String::from).collect();
85
+ let pertain: HashSet<String> =
86
+ ["of"].into_iter().map(String::from).collect();
87
+
88
+ Self {
89
+ jump, weekdays, months, hms, ampm, utczone, pertain,
90
+ tzoffset: HashMap::new(),
91
+ }
92
+ }
93
+ }
94
+
95
+ impl ParserInfo {
96
+ #[inline]
97
+ pub fn jump(&self, s: &str) -> bool {
98
+ lowercase_buf(s).is_some_and(|buf| self.jump.contains(lower_str(s, &buf)))
99
+ }
100
+
101
+ #[inline]
102
+ pub fn weekday(&self, s: &str) -> Option<usize> {
103
+ let buf = lowercase_buf(s)?;
104
+ let low = lower_str(s, &buf);
105
+ if let Some(&v) = self.weekdays.get(low) {
106
+ return Some(v);
107
+ }
108
+ if s.len() >= 4 {
109
+ if let Some(&v) = self.weekdays.get(&low[..3]) {
110
+ return Some(v);
111
+ }
112
+ }
113
+ None
114
+ }
115
+
116
+ #[inline]
117
+ pub fn month(&self, s: &str) -> Option<usize> {
118
+ let buf = lowercase_buf(s)?;
119
+ self.months.get(lower_str(s, &buf)).copied()
120
+ }
121
+
122
+ #[inline]
123
+ pub fn hms(&self, s: &str) -> Option<usize> {
124
+ let buf = lowercase_buf(s)?;
125
+ self.hms.get(lower_str(s, &buf)).copied()
126
+ }
127
+
128
+ #[inline]
129
+ pub fn ampm(&self, s: &str) -> Option<usize> {
130
+ let buf = lowercase_buf(s)?;
131
+ self.ampm.get(lower_str(s, &buf)).copied()
132
+ }
133
+
134
+ #[inline]
135
+ pub fn pertain(&self, s: &str) -> bool {
136
+ lowercase_buf(s).is_some_and(|buf| self.pertain.contains(lower_str(s, &buf)))
137
+ }
138
+
139
+ #[inline]
140
+ pub fn utczone(&self, s: &str) -> bool {
141
+ lowercase_buf(s).is_some_and(|buf| self.utczone.contains(lower_str(s, &buf)))
142
+ }
143
+
144
+ /// Look up a known timezone abbreviation. Returns offset in seconds.
145
+ /// UTC-equivalent zones return `Some(0)`. Matching is case-insensitive.
146
+ /// Single `lowercase_buf` call covers both utczone and tzoffset lookups.
147
+ #[inline]
148
+ pub fn tzoffset(&self, name: &str) -> Option<i32> {
149
+ let buf = lowercase_buf(name)?;
150
+ let low = lower_str(name, &buf);
151
+ if self.utczone.contains(low) {
152
+ return Some(0);
153
+ }
154
+ self.tzoffset.get(low).copied()
155
+ }
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Dispatch helpers — use ParserInfo when provided, PHF otherwise.
160
+ // ---------------------------------------------------------------------------
161
+
162
+ #[inline]
163
+ pub(super) fn do_jump(s: &str, info: Option<&ParserInfo>) -> bool {
164
+ match info {
165
+ Some(i) => i.jump(s),
166
+ None => lookup_jump(s),
167
+ }
168
+ }
169
+
170
+ #[inline]
171
+ pub(super) fn do_weekday(s: &str, info: Option<&ParserInfo>) -> Option<usize> {
172
+ match info {
173
+ Some(i) => i.weekday(s),
174
+ None => lookup_weekday(s),
175
+ }
176
+ }
177
+
178
+ #[inline]
179
+ pub(super) fn do_month(s: &str, info: Option<&ParserInfo>) -> Option<usize> {
180
+ match info {
181
+ Some(i) => i.month(s),
182
+ None => lookup_month(s),
183
+ }
184
+ }
185
+
186
+ #[inline]
187
+ pub(super) fn do_hms(s: &str, info: Option<&ParserInfo>) -> Option<usize> {
188
+ match info {
189
+ Some(i) => i.hms(s),
190
+ None => lookup_hms(s),
191
+ }
192
+ }
193
+
194
+ #[inline]
195
+ pub(super) fn do_ampm(s: &str, info: Option<&ParserInfo>) -> Option<usize> {
196
+ match info {
197
+ Some(i) => i.ampm(s),
198
+ None => lookup_ampm(s),
199
+ }
200
+ }
201
+
202
+ #[inline]
203
+ pub(super) fn do_pertain(s: &str, info: Option<&ParserInfo>) -> bool {
204
+ match info {
205
+ Some(i) => i.pertain(s),
206
+ None => lookup_pertain(s),
207
+ }
208
+ }
209
+
210
+ #[inline]
211
+ pub(super) fn do_utczone(s: &str, info: Option<&ParserInfo>) -> bool {
212
+ match info {
213
+ Some(i) => i.utczone(s),
214
+ None => lookup_utczone(s),
215
+ }
216
+ }
217
+
218
+ #[inline]
219
+ pub(super) fn do_tzoffset(name: &str, info: Option<&ParserInfo>) -> Option<i32> {
220
+ match info {
221
+ Some(i) => i.tzoffset(name),
222
+ None => None, // default PHF has no tzoffset map
223
+ }
224
+ }
225
+
226
+ #[cfg(test)]
227
+ mod tests {
228
+ use super::*;
229
+
230
+ #[test]
231
+ fn test_parserinfo_default_months() {
232
+ let info = ParserInfo::default();
233
+ assert_eq!(info.month("January"), Some(1));
234
+ assert_eq!(info.month("jan"), Some(1));
235
+ assert_eq!(info.month("DECEMBER"), Some(12));
236
+ assert_eq!(info.month("sept"), Some(9));
237
+ assert_eq!(info.month("xyz"), None);
238
+ }
239
+
240
+ #[test]
241
+ fn test_parserinfo_default_weekdays() {
242
+ let info = ParserInfo::default();
243
+ assert_eq!(info.weekday("Monday"), Some(0));
244
+ assert_eq!(info.weekday("fri"), Some(4));
245
+ assert_eq!(info.weekday("Frid"), Some(4)); // prefix match
246
+ assert_eq!(info.weekday("xyz"), None);
247
+ }
248
+
249
+ #[test]
250
+ fn test_parserinfo_default_jump() {
251
+ let info = ParserInfo::default();
252
+ assert!(info.jump("at"));
253
+ assert!(info.jump("on"));
254
+ assert!(info.jump(","));
255
+ assert!(!info.jump("foo"));
256
+ }
257
+
258
+ #[test]
259
+ fn test_parserinfo_default_utczone() {
260
+ let info = ParserInfo::default();
261
+ assert!(info.utczone("UTC"));
262
+ assert!(info.utczone("gmt"));
263
+ assert!(info.utczone("Z"));
264
+ assert!(!info.utczone("EST"));
265
+ }
266
+
267
+ #[test]
268
+ fn test_parserinfo_tzoffset() {
269
+ let mut info = ParserInfo::default();
270
+ info.tzoffset.insert("est".into(), -18000);
271
+ info.tzoffset.insert("cst".into(), -21600);
272
+
273
+ // Case-insensitive lookup
274
+ assert_eq!(info.tzoffset("EST"), Some(-18000));
275
+ assert_eq!(info.tzoffset("est"), Some(-18000));
276
+ assert_eq!(info.tzoffset("Est"), Some(-18000));
277
+ assert_eq!(info.tzoffset("CST"), Some(-21600));
278
+ assert_eq!(info.tzoffset("UTC"), Some(0)); // utczone fallback
279
+ assert_eq!(info.tzoffset("XYZ"), None);
280
+ }
281
+
282
+ #[test]
283
+ fn test_parserinfo_custom_months() {
284
+ let mut info = ParserInfo::default();
285
+ // Add German month names
286
+ info.months.insert("januar".into(), 1);
287
+ info.months.insert("februar".into(), 2);
288
+ info.months.insert("maerz".into(), 3);
289
+
290
+ assert_eq!(info.month("Januar"), Some(1));
291
+ assert_eq!(info.month("FEBRUAR"), Some(2));
292
+ assert_eq!(info.month("maerz"), Some(3));
293
+ // English still works
294
+ assert_eq!(info.month("January"), Some(1));
295
+ }
296
+
297
+ #[test]
298
+ fn test_dispatch_with_none_uses_phf() {
299
+ assert_eq!(do_month("January", None), Some(1));
300
+ assert_eq!(do_weekday("Monday", None), Some(0));
301
+ assert!(do_jump("at", None));
302
+ assert!(do_utczone("UTC", None));
303
+ assert_eq!(do_hms("hour", None), Some(0));
304
+ assert_eq!(do_ampm("AM", None), Some(0));
305
+ assert!(do_pertain("of", None));
306
+ assert_eq!(do_tzoffset("EST", None), None);
307
+ }
308
+
309
+ #[test]
310
+ fn test_dispatch_with_info_uses_custom() {
311
+ let mut info = ParserInfo::default();
312
+ info.tzoffset.insert("est".into(), -18000);
313
+
314
+ assert_eq!(do_tzoffset("EST", Some(&info)), Some(-18000));
315
+ assert_eq!(do_tzoffset("est", Some(&info)), Some(-18000));
316
+ assert_eq!(do_month("January", Some(&info)), Some(1));
317
+ }
318
+ }