1#![cfg(feature = "jiff-02")]
2
3#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"jiff-02\"] }")]
14use crate::exceptions::{PyTypeError, PyValueError};
49use crate::pybacked::PyBackedStr;
50use crate::types::{PyAnyMethods, PyNone};
51use crate::types::{PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo, PyTzInfoAccess};
52#[cfg(not(Py_LIMITED_API))]
53use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess};
54use crate::{intern, Bound, FromPyObject, IntoPyObject, PyAny, PyErr, PyResult, Python};
55use jiff::civil::{Date, DateTime, Time};
56use jiff::tz::{Offset, TimeZone};
57use jiff::{SignedDuration, Span, Timestamp, Zoned};
58#[cfg(feature = "jiff-02")]
59use jiff_02 as jiff;
60
61fn datetime_to_pydatetime<'py>(
62 py: Python<'py>,
63 datetime: &DateTime,
64 fold: bool,
65 timezone: Option<&TimeZone>,
66) -> PyResult<Bound<'py, PyDateTime>> {
67 PyDateTime::new_with_fold(
68 py,
69 datetime.year().into(),
70 datetime.month().try_into()?,
71 datetime.day().try_into()?,
72 datetime.hour().try_into()?,
73 datetime.minute().try_into()?,
74 datetime.second().try_into()?,
75 (datetime.subsec_nanosecond() / 1000).try_into()?,
76 timezone
77 .map(|tz| tz.into_pyobject(py))
78 .transpose()?
79 .as_ref(),
80 fold,
81 )
82}
83
84#[cfg(not(Py_LIMITED_API))]
85fn pytime_to_time(time: &impl PyTimeAccess) -> PyResult<Time> {
86 Ok(Time::new(
87 time.get_hour().try_into()?,
88 time.get_minute().try_into()?,
89 time.get_second().try_into()?,
90 (time.get_microsecond() * 1000).try_into()?,
91 )?)
92}
93
94#[cfg(Py_LIMITED_API)]
95fn pytime_to_time(time: &Bound<'_, PyAny>) -> PyResult<Time> {
96 let py = time.py();
97 Ok(Time::new(
98 time.getattr(intern!(py, "hour"))?.extract()?,
99 time.getattr(intern!(py, "minute"))?.extract()?,
100 time.getattr(intern!(py, "second"))?.extract()?,
101 time.getattr(intern!(py, "microsecond"))?.extract::<i32>()? * 1000,
102 )?)
103}
104
105impl<'py> IntoPyObject<'py> for Timestamp {
106 type Target = PyDateTime;
107 type Output = Bound<'py, Self::Target>;
108 type Error = PyErr;
109
110 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
111 (&self).into_pyobject(py)
112 }
113}
114
115impl<'py> IntoPyObject<'py> for &Timestamp {
116 type Target = PyDateTime;
117 type Output = Bound<'py, Self::Target>;
118 type Error = PyErr;
119
120 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
121 self.to_zoned(TimeZone::UTC).into_pyobject(py)
122 }
123}
124
125impl<'py> FromPyObject<'py> for Timestamp {
126 fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
127 let zoned = ob.extract::<Zoned>()?;
128 Ok(zoned.timestamp())
129 }
130}
131
132impl<'py> IntoPyObject<'py> for Date {
133 type Target = PyDate;
134 type Output = Bound<'py, Self::Target>;
135 type Error = PyErr;
136
137 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
138 (&self).into_pyobject(py)
139 }
140}
141
142impl<'py> IntoPyObject<'py> for &Date {
143 type Target = PyDate;
144 type Output = Bound<'py, Self::Target>;
145 type Error = PyErr;
146
147 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
148 PyDate::new(
149 py,
150 self.year().into(),
151 self.month().try_into()?,
152 self.day().try_into()?,
153 )
154 }
155}
156
157impl<'py> FromPyObject<'py> for Date {
158 fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
159 let date = ob.downcast::<PyDate>()?;
160
161 #[cfg(not(Py_LIMITED_API))]
162 {
163 Ok(Date::new(
164 date.get_year().try_into()?,
165 date.get_month().try_into()?,
166 date.get_day().try_into()?,
167 )?)
168 }
169
170 #[cfg(Py_LIMITED_API)]
171 {
172 let py = date.py();
173 Ok(Date::new(
174 date.getattr(intern!(py, "year"))?.extract()?,
175 date.getattr(intern!(py, "month"))?.extract()?,
176 date.getattr(intern!(py, "day"))?.extract()?,
177 )?)
178 }
179 }
180}
181
182impl<'py> IntoPyObject<'py> for Time {
183 type Target = PyTime;
184 type Output = Bound<'py, Self::Target>;
185 type Error = PyErr;
186
187 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
188 (&self).into_pyobject(py)
189 }
190}
191
192impl<'py> IntoPyObject<'py> for &Time {
193 type Target = PyTime;
194 type Output = Bound<'py, Self::Target>;
195 type Error = PyErr;
196
197 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
198 PyTime::new(
199 py,
200 self.hour().try_into()?,
201 self.minute().try_into()?,
202 self.second().try_into()?,
203 (self.subsec_nanosecond() / 1000).try_into()?,
204 None,
205 )
206 }
207}
208
209impl<'py> FromPyObject<'py> for Time {
210 fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
211 let ob = ob.downcast::<PyTime>()?;
212
213 pytime_to_time(ob)
214 }
215}
216
217impl<'py> IntoPyObject<'py> for DateTime {
218 type Target = PyDateTime;
219 type Output = Bound<'py, Self::Target>;
220 type Error = PyErr;
221
222 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
223 (&self).into_pyobject(py)
224 }
225}
226
227impl<'py> IntoPyObject<'py> for &DateTime {
228 type Target = PyDateTime;
229 type Output = Bound<'py, Self::Target>;
230 type Error = PyErr;
231
232 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
233 datetime_to_pydatetime(py, self, false, None)
234 }
235}
236
237impl<'py> FromPyObject<'py> for DateTime {
238 fn extract_bound(dt: &Bound<'py, PyAny>) -> PyResult<Self> {
239 let dt = dt.downcast::<PyDateTime>()?;
240 let has_tzinfo = dt.get_tzinfo().is_some();
241
242 if has_tzinfo {
243 return Err(PyTypeError::new_err("expected a datetime without tzinfo"));
244 }
245
246 Ok(DateTime::from_parts(dt.extract()?, pytime_to_time(dt)?))
247 }
248}
249
250impl<'py> IntoPyObject<'py> for Zoned {
251 type Target = PyDateTime;
252 type Output = Bound<'py, Self::Target>;
253 type Error = PyErr;
254
255 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
256 (&self).into_pyobject(py)
257 }
258}
259
260impl<'py> IntoPyObject<'py> for &Zoned {
261 type Target = PyDateTime;
262 type Output = Bound<'py, Self::Target>;
263 type Error = PyErr;
264
265 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
266 fn fold(zoned: &Zoned) -> Option<bool> {
267 let prev = zoned.time_zone().preceding(zoned.timestamp()).next()?;
268 let next = zoned.time_zone().following(prev.timestamp()).next()?;
269 let start_of_current_offset = if next.timestamp() == zoned.timestamp() {
270 next.timestamp()
271 } else {
272 prev.timestamp()
273 };
274 Some(zoned.timestamp() + (zoned.offset() - prev.offset()) <= start_of_current_offset)
275 }
276
277 datetime_to_pydatetime(
278 py,
279 &self.datetime(),
280 fold(self).unwrap_or(false),
281 Some(self.time_zone()),
282 )
283 }
284}
285
286impl<'py> FromPyObject<'py> for Zoned {
287 fn extract_bound(dt: &Bound<'py, PyAny>) -> PyResult<Self> {
288 let dt = dt.downcast::<PyDateTime>()?;
289
290 let tz = dt
291 .get_tzinfo()
292 .map(|tz| tz.extract::<TimeZone>())
293 .unwrap_or_else(|| {
294 Err(PyTypeError::new_err(
295 "expected a datetime with non-None tzinfo",
296 ))
297 })?;
298 let datetime = DateTime::from_parts(dt.extract()?, pytime_to_time(dt)?);
299 let zoned = tz.into_ambiguous_zoned(datetime);
300
301 #[cfg(not(Py_LIMITED_API))]
302 let fold = dt.get_fold();
303
304 #[cfg(Py_LIMITED_API)]
305 let fold = dt.getattr(intern!(dt.py(), "fold"))?.extract::<usize>()? > 0;
306
307 if fold {
308 Ok(zoned.later()?)
309 } else {
310 Ok(zoned.earlier()?)
311 }
312 }
313}
314
315impl<'py> IntoPyObject<'py> for TimeZone {
316 type Target = PyTzInfo;
317 type Output = Bound<'py, Self::Target>;
318 type Error = PyErr;
319
320 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
321 (&self).into_pyobject(py)
322 }
323}
324
325impl<'py> IntoPyObject<'py> for &TimeZone {
326 type Target = PyTzInfo;
327 type Output = Bound<'py, Self::Target>;
328 type Error = PyErr;
329
330 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
331 if self == &TimeZone::UTC {
332 return Ok(PyTzInfo::utc(py)?.to_owned());
333 }
334
335 if let Some(iana_name) = self.iana_name() {
336 return PyTzInfo::timezone(py, iana_name);
337 }
338
339 self.to_fixed_offset()?.into_pyobject(py)
340 }
341}
342
343impl<'py> FromPyObject<'py> for TimeZone {
344 fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
345 let ob = ob.downcast::<PyTzInfo>()?;
346
347 let attr = intern!(ob.py(), "key");
348 if ob.hasattr(attr)? {
349 Ok(TimeZone::get(&ob.getattr(attr)?.extract::<PyBackedStr>()?)?)
350 } else {
351 Ok(ob.extract::<Offset>()?.to_time_zone())
352 }
353 }
354}
355
356impl<'py> IntoPyObject<'py> for &Offset {
357 type Target = PyTzInfo;
358 type Output = Bound<'py, Self::Target>;
359 type Error = PyErr;
360
361 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
362 if self == &Offset::UTC {
363 return Ok(PyTzInfo::utc(py)?.to_owned());
364 }
365
366 PyTzInfo::fixed_offset(py, self.duration_since(Offset::UTC))
367 }
368}
369
370impl<'py> IntoPyObject<'py> for Offset {
371 type Target = PyTzInfo;
372 type Output = Bound<'py, Self::Target>;
373 type Error = PyErr;
374
375 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
376 (&self).into_pyobject(py)
377 }
378}
379
380impl<'py> FromPyObject<'py> for Offset {
381 fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
382 let py = ob.py();
383 let ob = ob.downcast::<PyTzInfo>()?;
384
385 let py_timedelta = ob.call_method1(intern!(py, "utcoffset"), (PyNone::get(py),))?;
386 if py_timedelta.is_none() {
387 return Err(PyTypeError::new_err(format!(
388 "{ob:?} is not a fixed offset timezone"
389 )));
390 }
391
392 let total_seconds = py_timedelta.extract::<SignedDuration>()?.as_secs();
393 debug_assert!(
394 (total_seconds / 3600).abs() <= 24,
395 "Offset must be between -24 hours and 24 hours but was {}h",
396 total_seconds / 3600
397 );
398 Ok(Offset::from_seconds(total_seconds as i32)?)
400 }
401}
402
403impl<'py> IntoPyObject<'py> for &SignedDuration {
404 type Target = PyDelta;
405 type Output = Bound<'py, Self::Target>;
406 type Error = PyErr;
407
408 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
409 let total_seconds = self.as_secs();
410 let days: i32 = (total_seconds / (24 * 60 * 60)).try_into()?;
411 let seconds: i32 = (total_seconds % (24 * 60 * 60)).try_into()?;
412 let microseconds = self.subsec_micros();
413
414 PyDelta::new(py, days, seconds, microseconds, true)
415 }
416}
417
418impl<'py> IntoPyObject<'py> for SignedDuration {
419 type Target = PyDelta;
420 type Output = Bound<'py, Self::Target>;
421 type Error = PyErr;
422
423 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
424 (&self).into_pyobject(py)
425 }
426}
427
428impl<'py> FromPyObject<'py> for SignedDuration {
429 fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
430 let delta = ob.downcast::<PyDelta>()?;
431
432 #[cfg(not(Py_LIMITED_API))]
433 let (seconds, microseconds) = {
434 let days = delta.get_days() as i64;
435 let seconds = delta.get_seconds() as i64;
436 let microseconds = delta.get_microseconds();
437 (days * 24 * 60 * 60 + seconds, microseconds)
438 };
439
440 #[cfg(Py_LIMITED_API)]
441 let (seconds, microseconds) = {
442 let py = delta.py();
443 let days = delta.getattr(intern!(py, "days"))?.extract::<i64>()?;
444 let seconds = delta.getattr(intern!(py, "seconds"))?.extract::<i64>()?;
445 let microseconds = ob.getattr(intern!(py, "microseconds"))?.extract::<i32>()?;
446 (days * 24 * 60 * 60 + seconds, microseconds)
447 };
448
449 Ok(SignedDuration::new(seconds, microseconds * 1000))
450 }
451}
452
453impl<'py> FromPyObject<'py> for Span {
454 fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
455 let duration = ob.extract::<SignedDuration>()?;
456 Ok(duration.try_into()?)
457 }
458}
459
460impl From<jiff::Error> for PyErr {
461 fn from(e: jiff::Error) -> Self {
462 PyValueError::new_err(e.to_string())
463 }
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469 use crate::{types::PyTuple, BoundObject};
470 use jiff::tz::Offset;
471 use std::cmp::Ordering;
472
473 #[test]
474 #[cfg(all(Py_3_9, not(target_os = "windows")))]
478 fn test_zoneinfo_is_not_fixed_offset() {
479 use crate::ffi;
480 use crate::types::any::PyAnyMethods;
481 use crate::types::dict::PyDictMethods;
482
483 Python::with_gil(|py| {
484 let locals = crate::types::PyDict::new(py);
485 py.run(
486 ffi::c_str!("import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')"),
487 None,
488 Some(&locals),
489 )
490 .unwrap();
491 let result: PyResult<Offset> = locals.get_item("zi").unwrap().unwrap().extract();
492 assert!(result.is_err());
493 let res = result.err().unwrap();
494 let msg = res.value(py).repr().unwrap().to_string();
496 assert_eq!(msg, "TypeError(\"zoneinfo.ZoneInfo(key='Europe/London') is not a fixed offset timezone\")");
497 });
498 }
499
500 #[test]
501 fn test_timezone_aware_to_naive_fails() {
502 Python::with_gil(|py| {
505 let py_datetime =
506 new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0, python_utc(py)));
507 let res: PyResult<DateTime> = py_datetime.extract();
509 assert_eq!(
510 res.unwrap_err().value(py).repr().unwrap().to_string(),
511 "TypeError('expected a datetime without tzinfo')"
512 );
513 });
514 }
515
516 #[test]
517 fn test_naive_to_timezone_aware_fails() {
518 Python::with_gil(|py| {
521 let py_datetime = new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0));
522 let res: PyResult<Zoned> = py_datetime.extract();
523 assert_eq!(
524 res.unwrap_err().value(py).repr().unwrap().to_string(),
525 "TypeError('expected a datetime with non-None tzinfo')"
526 );
527 });
528 }
529
530 #[test]
531 fn test_invalid_types_fail() {
532 Python::with_gil(|py| {
533 let none = py.None().into_bound(py);
534 assert_eq!(
535 none.extract::<Span>().unwrap_err().to_string(),
536 "TypeError: 'NoneType' object cannot be converted to 'PyDelta'"
537 );
538 assert_eq!(
539 none.extract::<Offset>().unwrap_err().to_string(),
540 "TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'"
541 );
542 assert_eq!(
543 none.extract::<TimeZone>().unwrap_err().to_string(),
544 "TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'"
545 );
546 assert_eq!(
547 none.extract::<Time>().unwrap_err().to_string(),
548 "TypeError: 'NoneType' object cannot be converted to 'PyTime'"
549 );
550 assert_eq!(
551 none.extract::<Date>().unwrap_err().to_string(),
552 "TypeError: 'NoneType' object cannot be converted to 'PyDate'"
553 );
554 assert_eq!(
555 none.extract::<DateTime>().unwrap_err().to_string(),
556 "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'"
557 );
558 assert_eq!(
559 none.extract::<Zoned>().unwrap_err().to_string(),
560 "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'"
561 );
562 });
563 }
564
565 #[test]
566 fn test_pyo3_date_into_pyobject() {
567 let eq_ymd = |name: &'static str, year, month, day| {
568 Python::with_gil(|py| {
569 let date = Date::new(year, month, day)
570 .unwrap()
571 .into_pyobject(py)
572 .unwrap();
573 let py_date = new_py_datetime_ob(py, "date", (year, month, day));
574 assert_eq!(
575 date.compare(&py_date).unwrap(),
576 Ordering::Equal,
577 "{name}: {date} != {py_date}"
578 );
579 })
580 };
581
582 eq_ymd("past date", 2012, 2, 29);
583 eq_ymd("min date", 1, 1, 1);
584 eq_ymd("future date", 3000, 6, 5);
585 eq_ymd("max date", 9999, 12, 31);
586 }
587
588 #[test]
589 fn test_pyo3_date_frompyobject() {
590 let eq_ymd = |name: &'static str, year, month, day| {
591 Python::with_gil(|py| {
592 let py_date = new_py_datetime_ob(py, "date", (year, month, day));
593 let py_date: Date = py_date.extract().unwrap();
594 let date = Date::new(year, month, day).unwrap();
595 assert_eq!(py_date, date, "{name}: {date} != {py_date}");
596 })
597 };
598
599 eq_ymd("past date", 2012, 2, 29);
600 eq_ymd("min date", 1, 1, 1);
601 eq_ymd("future date", 3000, 6, 5);
602 eq_ymd("max date", 9999, 12, 31);
603 }
604
605 #[test]
606 fn test_pyo3_datetime_into_pyobject_utc() {
607 Python::with_gil(|py| {
608 let check_utc =
609 |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
610 let datetime = DateTime::new(year, month, day, hour, minute, second, ms * 1000)
611 .unwrap()
612 .to_zoned(TimeZone::UTC)
613 .unwrap();
614 let datetime = datetime.into_pyobject(py).unwrap();
615 let py_datetime = new_py_datetime_ob(
616 py,
617 "datetime",
618 (
619 year,
620 month,
621 day,
622 hour,
623 minute,
624 second,
625 py_ms,
626 python_utc(py),
627 ),
628 );
629 assert_eq!(
630 datetime.compare(&py_datetime).unwrap(),
631 Ordering::Equal,
632 "{name}: {datetime} != {py_datetime}"
633 );
634 };
635
636 check_utc("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999);
637 })
638 }
639
640 #[test]
641 fn test_pyo3_datetime_into_pyobject_fixed_offset() {
642 Python::with_gil(|py| {
643 let check_fixed_offset =
644 |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
645 let offset = Offset::from_seconds(3600).unwrap();
646 let datetime = DateTime::new(year, month, day, hour, minute, second, ms * 1000)
647 .map_err(|e| {
648 eprintln!("{name}: {e}");
649 e
650 })
651 .unwrap()
652 .to_zoned(offset.to_time_zone())
653 .unwrap();
654 let datetime = datetime.into_pyobject(py).unwrap();
655 let py_tz = offset.into_pyobject(py).unwrap();
656 let py_datetime = new_py_datetime_ob(
657 py,
658 "datetime",
659 (year, month, day, hour, minute, second, py_ms, py_tz),
660 );
661 assert_eq!(
662 datetime.compare(&py_datetime).unwrap(),
663 Ordering::Equal,
664 "{name}: {datetime} != {py_datetime}"
665 );
666 };
667
668 check_fixed_offset("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999);
669 })
670 }
671
672 #[test]
673 #[cfg(all(Py_3_9, not(windows)))]
674 fn test_pyo3_datetime_into_pyobject_tz() {
675 Python::with_gil(|py| {
676 let datetime = DateTime::new(2024, 12, 11, 23, 3, 13, 0)
677 .unwrap()
678 .to_zoned(TimeZone::get("Europe/London").unwrap())
679 .unwrap();
680 let datetime = datetime.into_pyobject(py).unwrap();
681 let py_datetime = new_py_datetime_ob(
682 py,
683 "datetime",
684 (
685 2024,
686 12,
687 11,
688 23,
689 3,
690 13,
691 0,
692 python_zoneinfo(py, "Europe/London"),
693 ),
694 );
695 assert_eq!(datetime.compare(&py_datetime).unwrap(), Ordering::Equal);
696 })
697 }
698
699 #[test]
700 fn test_pyo3_datetime_frompyobject_utc() {
701 Python::with_gil(|py| {
702 let year = 2014;
703 let month = 5;
704 let day = 6;
705 let hour = 7;
706 let minute = 8;
707 let second = 9;
708 let micro = 999_999;
709 let tz_utc = PyTzInfo::utc(py).unwrap();
710 let py_datetime = new_py_datetime_ob(
711 py,
712 "datetime",
713 (year, month, day, hour, minute, second, micro, tz_utc),
714 );
715 let py_datetime: Zoned = py_datetime.extract().unwrap();
716 let datetime = DateTime::new(year, month, day, hour, minute, second, micro * 1000)
717 .unwrap()
718 .to_zoned(TimeZone::UTC)
719 .unwrap();
720 assert_eq!(py_datetime, datetime,);
721 })
722 }
723
724 #[test]
725 #[cfg(all(Py_3_9, not(windows)))]
726 fn test_ambiguous_datetime_to_pyobject() {
727 use std::str::FromStr;
728 let dates = [
729 Zoned::from_str("2020-10-24 23:00:00[UTC]").unwrap(),
730 Zoned::from_str("2020-10-25 00:00:00[UTC]").unwrap(),
731 Zoned::from_str("2020-10-25 01:00:00[UTC]").unwrap(),
732 Zoned::from_str("2020-10-25 02:00:00[UTC]").unwrap(),
733 ];
734
735 let tz = TimeZone::get("Europe/London").unwrap();
736 let dates = dates.map(|dt| dt.with_time_zone(tz.clone()));
737
738 assert_eq!(
739 dates.clone().map(|ref dt| dt.to_string()),
740 [
741 "2020-10-25T00:00:00+01:00[Europe/London]",
742 "2020-10-25T01:00:00+01:00[Europe/London]",
743 "2020-10-25T01:00:00+00:00[Europe/London]",
744 "2020-10-25T02:00:00+00:00[Europe/London]",
745 ]
746 );
747
748 let dates = Python::with_gil(|py| {
749 let pydates = dates.map(|dt| dt.into_pyobject(py).unwrap());
750 assert_eq!(
751 pydates
752 .clone()
753 .map(|dt| dt.getattr("hour").unwrap().extract::<usize>().unwrap()),
754 [0, 1, 1, 2]
755 );
756
757 assert_eq!(
758 pydates
759 .clone()
760 .map(|dt| dt.getattr("fold").unwrap().extract::<usize>().unwrap() > 0),
761 [false, false, true, false]
762 );
763
764 pydates.map(|dt| dt.extract::<Zoned>().unwrap())
765 });
766
767 assert_eq!(
768 dates.map(|dt| dt.to_string()),
769 [
770 "2020-10-25T00:00:00+01:00[Europe/London]",
771 "2020-10-25T01:00:00+01:00[Europe/London]",
772 "2020-10-25T01:00:00+00:00[Europe/London]",
773 "2020-10-25T02:00:00+00:00[Europe/London]",
774 ]
775 );
776 }
777
778 #[test]
779 fn test_pyo3_datetime_frompyobject_fixed_offset() {
780 Python::with_gil(|py| {
781 let year = 2014;
782 let month = 5;
783 let day = 6;
784 let hour = 7;
785 let minute = 8;
786 let second = 9;
787 let micro = 999_999;
788 let offset = Offset::from_seconds(3600).unwrap();
789 let py_tz = offset.into_pyobject(py).unwrap();
790 let py_datetime = new_py_datetime_ob(
791 py,
792 "datetime",
793 (year, month, day, hour, minute, second, micro, py_tz),
794 );
795 let datetime_from_py: Zoned = py_datetime.extract().unwrap();
796 let datetime =
797 DateTime::new(year, month, day, hour, minute, second, micro * 1000).unwrap();
798 let datetime = datetime.to_zoned(offset.to_time_zone()).unwrap();
799
800 assert_eq!(datetime_from_py, datetime);
801 })
802 }
803
804 #[test]
805 fn test_pyo3_offset_fixed_into_pyobject() {
806 Python::with_gil(|py| {
807 let offset = Offset::from_seconds(3600)
809 .unwrap()
810 .into_pyobject(py)
811 .unwrap();
812 let td = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
814 let py_timedelta = new_py_datetime_ob(py, "timezone", (td,));
815 assert!(offset.eq(py_timedelta).unwrap());
817
818 let offset = Offset::from_seconds(-3600)
820 .unwrap()
821 .into_pyobject(py)
822 .unwrap();
823 let td = new_py_datetime_ob(py, "timedelta", (0, -3600, 0));
824 let py_timedelta = new_py_datetime_ob(py, "timezone", (td,));
825 assert!(offset.eq(py_timedelta).unwrap());
826 })
827 }
828
829 #[test]
830 fn test_pyo3_offset_fixed_frompyobject() {
831 Python::with_gil(|py| {
832 let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
833 let py_tzinfo = new_py_datetime_ob(py, "timezone", (py_timedelta,));
834 let offset: Offset = py_tzinfo.extract().unwrap();
835 assert_eq!(Offset::from_seconds(3600).unwrap(), offset);
836 })
837 }
838
839 #[test]
840 fn test_pyo3_offset_utc_into_pyobject() {
841 Python::with_gil(|py| {
842 let utc = Offset::UTC.into_pyobject(py).unwrap();
843 let py_utc = python_utc(py);
844 assert!(utc.is(&py_utc));
845 })
846 }
847
848 #[test]
849 fn test_pyo3_offset_utc_frompyobject() {
850 Python::with_gil(|py| {
851 let py_utc = python_utc(py);
852 let py_utc: Offset = py_utc.extract().unwrap();
853 assert_eq!(Offset::UTC, py_utc);
854
855 let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 0, 0));
856 let py_timezone_utc = new_py_datetime_ob(py, "timezone", (py_timedelta,));
857 let py_timezone_utc: Offset = py_timezone_utc.extract().unwrap();
858 assert_eq!(Offset::UTC, py_timezone_utc);
859
860 let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
861 let py_timezone = new_py_datetime_ob(py, "timezone", (py_timedelta,));
862 assert_ne!(Offset::UTC, py_timezone.extract::<Offset>().unwrap());
863 })
864 }
865
866 #[test]
867 fn test_pyo3_time_into_pyobject() {
868 Python::with_gil(|py| {
869 let check_time = |name: &'static str, hour, minute, second, ms, py_ms| {
870 let time = Time::new(hour, minute, second, ms * 1000)
871 .unwrap()
872 .into_pyobject(py)
873 .unwrap();
874 let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, py_ms));
875 assert!(time.eq(&py_time).unwrap(), "{name}: {time} != {py_time}");
876 };
877
878 check_time("regular", 3, 5, 7, 999_999, 999_999);
879 })
880 }
881
882 #[test]
883 fn test_pyo3_time_frompyobject() {
884 let hour = 3;
885 let minute = 5;
886 let second = 7;
887 let micro = 999_999;
888 Python::with_gil(|py| {
889 let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, micro));
890 let py_time: Time = py_time.extract().unwrap();
891 let time = Time::new(hour, minute, second, micro * 1000).unwrap();
892 assert_eq!(py_time, time);
893 })
894 }
895
896 fn new_py_datetime_ob<'py, A>(py: Python<'py>, name: &str, args: A) -> Bound<'py, PyAny>
897 where
898 A: IntoPyObject<'py, Target = PyTuple>,
899 {
900 py.import("datetime")
901 .unwrap()
902 .getattr(name)
903 .unwrap()
904 .call1(
905 args.into_pyobject(py)
906 .map_err(Into::into)
907 .unwrap()
908 .into_bound(),
909 )
910 .unwrap()
911 }
912
913 fn python_utc(py: Python<'_>) -> Bound<'_, PyAny> {
914 py.import("datetime")
915 .unwrap()
916 .getattr("timezone")
917 .unwrap()
918 .getattr("utc")
919 .unwrap()
920 }
921
922 #[cfg(all(Py_3_9, not(windows)))]
923 fn python_zoneinfo<'py>(py: Python<'py>, timezone: &str) -> Bound<'py, PyAny> {
924 py.import("zoneinfo")
925 .unwrap()
926 .getattr("ZoneInfo")
927 .unwrap()
928 .call1((timezone,))
929 .unwrap()
930 }
931
932 #[cfg(not(any(target_arch = "wasm32", Py_GIL_DISABLED)))]
933 mod proptests {
934 use super::*;
935 use crate::types::IntoPyDict;
936 use jiff::tz::TimeZoneTransition;
937 use jiff::SpanRelativeTo;
938 use proptest::prelude::*;
939 use std::ffi::CString;
940
941 fn try_date(year: i32, month: u32, day: u32) -> PyResult<Date> {
943 Ok(Date::new(
944 year.try_into()?,
945 month.try_into()?,
946 day.try_into()?,
947 )?)
948 }
949
950 fn try_time(hour: u32, min: u32, sec: u32, micro: u32) -> PyResult<Time> {
951 Ok(Time::new(
952 hour.try_into()?,
953 min.try_into()?,
954 sec.try_into()?,
955 (micro * 1000).try_into()?,
956 )?)
957 }
958
959 prop_compose! {
960 fn timezone_transitions(timezone: &TimeZone)
961 (year in 1900i16..=2100i16, month in 1i8..=12i8)
962 -> TimeZoneTransition<'_> {
963 let datetime = DateTime::new(year, month, 1, 0, 0, 0, 0).unwrap();
964 let timestamp= timezone.to_zoned(datetime).unwrap().timestamp();
965 timezone.following(timestamp).next().unwrap()
966 }
967 }
968
969 proptest! {
970
971 #[test]
973 fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) {
974 Python::with_gil(|py| {
975
976 let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py).unwrap();
977 let code = format!("datetime.datetime.fromtimestamp({timestamp}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={timedelta})))");
978 let t = py.eval(&CString::new(code).unwrap(), Some(&globals), None).unwrap();
979
980 let py_iso_str = t.call_method0("isoformat").unwrap();
982
983 let rust_iso_str = t.extract::<Zoned>().unwrap().strftime("%Y-%m-%dT%H:%M:%S%:z").to_string();
985
986 assert_eq!(py_iso_str.to_string(), rust_iso_str);
988 })
989 }
990
991 #[test]
992 fn test_duration_roundtrip(days in -999999999i64..=999999999i64) {
993 Python::with_gil(|py| {
996 let dur = SignedDuration::new(days * 24 * 60 * 60, 0);
997 let py_delta = dur.into_pyobject(py).unwrap();
998 let roundtripped: SignedDuration = py_delta.extract().expect("Round trip");
999 assert_eq!(dur, roundtripped);
1000 })
1001 }
1002
1003 #[test]
1004 fn test_span_roundtrip(days in -999999999i64..=999999999i64) {
1005 Python::with_gil(|py| {
1008 if let Ok(span) = Span::new().try_days(days) {
1009 let relative_to = SpanRelativeTo::days_are_24_hours();
1010 let jiff_duration = span.to_duration(relative_to).unwrap();
1011 let py_delta = jiff_duration.into_pyobject(py).unwrap();
1012 let roundtripped: Span = py_delta.extract().expect("Round trip");
1013 assert_eq!(span.compare((roundtripped, relative_to)).unwrap(), Ordering::Equal);
1014 }
1015 })
1016 }
1017
1018 #[test]
1019 fn test_fixed_offset_roundtrip(secs in -86399i32..=86399i32) {
1020 Python::with_gil(|py| {
1021 let offset = Offset::from_seconds(secs).unwrap();
1022 let py_offset = offset.into_pyobject(py).unwrap();
1023 let roundtripped: Offset = py_offset.extract().expect("Round trip");
1024 assert_eq!(offset, roundtripped);
1025 })
1026 }
1027
1028 #[test]
1029 fn test_naive_date_roundtrip(
1030 year in 1i32..=9999i32,
1031 month in 1u32..=12u32,
1032 day in 1u32..=31u32
1033 ) {
1034 Python::with_gil(|py| {
1037 if let Ok(date) = try_date(year, month, day) {
1038 let py_date = date.into_pyobject(py).unwrap();
1039 let roundtripped: Date = py_date.extract().expect("Round trip");
1040 assert_eq!(date, roundtripped);
1041 }
1042 })
1043 }
1044
1045 #[test]
1046 fn test_naive_time_roundtrip(
1047 hour in 0u32..=23u32,
1048 min in 0u32..=59u32,
1049 sec in 0u32..=59u32,
1050 micro in 0u32..=1_999_999u32
1051 ) {
1052 Python::with_gil(|py| {
1053 if let Ok(time) = try_time(hour, min, sec, micro) {
1054 let py_time = time.into_pyobject(py).unwrap();
1055 let roundtripped: Time = py_time.extract().expect("Round trip");
1056 assert_eq!(time, roundtripped);
1057 }
1058 })
1059 }
1060
1061 #[test]
1062 fn test_naive_datetime_roundtrip(
1063 year in 1i32..=9999i32,
1064 month in 1u32..=12u32,
1065 day in 1u32..=31u32,
1066 hour in 0u32..=24u32,
1067 min in 0u32..=60u32,
1068 sec in 0u32..=60u32,
1069 micro in 0u32..=999_999u32
1070 ) {
1071 Python::with_gil(|py| {
1072 let date_opt = try_date(year, month, day);
1073 let time_opt = try_time(hour, min, sec, micro);
1074 if let (Ok(date), Ok(time)) = (date_opt, time_opt) {
1075 let dt = DateTime::from_parts(date, time);
1076 let pydt = dt.into_pyobject(py).unwrap();
1077 let roundtripped: DateTime = pydt.extract().expect("Round trip");
1078 assert_eq!(dt, roundtripped);
1079 }
1080 })
1081 }
1082
1083 #[test]
1084 fn test_utc_datetime_roundtrip(
1085 year in 1i32..=9999i32,
1086 month in 1u32..=12u32,
1087 day in 1u32..=31u32,
1088 hour in 0u32..=23u32,
1089 min in 0u32..=59u32,
1090 sec in 0u32..=59u32,
1091 micro in 0u32..=1_999_999u32
1092 ) {
1093 Python::with_gil(|py| {
1094 let date_opt = try_date(year, month, day);
1095 let time_opt = try_time(hour, min, sec, micro);
1096 if let (Ok(date), Ok(time)) = (date_opt, time_opt) {
1097 let dt: Zoned = DateTime::from_parts(date, time).to_zoned(TimeZone::UTC).unwrap();
1098 let py_dt = (&dt).into_pyobject(py).unwrap();
1099 let roundtripped: Zoned = py_dt.extract().expect("Round trip");
1100 assert_eq!(dt, roundtripped);
1101 }
1102 })
1103 }
1104
1105 #[test]
1106 fn test_fixed_offset_datetime_roundtrip(
1107 year in 1i32..=9999i32,
1108 month in 1u32..=12u32,
1109 day in 1u32..=31u32,
1110 hour in 0u32..=23u32,
1111 min in 0u32..=59u32,
1112 sec in 0u32..=59u32,
1113 micro in 0u32..=1_999_999u32,
1114 offset_secs in -86399i32..=86399i32
1115 ) {
1116 Python::with_gil(|py| {
1117 let date_opt = try_date(year, month, day);
1118 let time_opt = try_time(hour, min, sec, micro);
1119 let offset = Offset::from_seconds(offset_secs).unwrap();
1120 if let (Ok(date), Ok(time)) = (date_opt, time_opt) {
1121 let dt: Zoned = DateTime::from_parts(date, time).to_zoned(offset.to_time_zone()).unwrap();
1122 let py_dt = (&dt).into_pyobject(py).unwrap();
1123 let roundtripped: Zoned = py_dt.extract().expect("Round trip");
1124 assert_eq!(dt, roundtripped);
1125 }
1126 })
1127 }
1128
1129 #[test]
1130 #[cfg(all(Py_3_9, not(windows)))]
1131 fn test_zoned_datetime_roundtrip_around_timezone_transition(
1132 (timezone, transition) in prop_oneof![
1133 Just(&TimeZone::get("Europe/London").unwrap()),
1134 Just(&TimeZone::get("America/New_York").unwrap()),
1135 Just(&TimeZone::get("Australia/Sydney").unwrap()),
1136 ].prop_flat_map(|tz| (Just(tz), timezone_transitions(tz))),
1137 hour in -2i32..=2i32,
1138 min in 0u32..=59u32,
1139 ) {
1140
1141 Python::with_gil(|py| {
1142 let transition_moment = transition.timestamp();
1143 let zoned = (transition_moment - Span::new().hours(hour).minutes(min))
1144 .to_zoned(timezone.clone());
1145
1146 let py_dt = (&zoned).into_pyobject(py).unwrap();
1147 let roundtripped: Zoned = py_dt.extract().expect("Round trip");
1148 assert_eq!(zoned, roundtripped);
1149 })
1150
1151 }
1152 }
1153 }
1154}