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