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