|
1 | 1 | use pyo3::intern;
|
2 | 2 | use pyo3::prelude::*;
|
3 |
| -use pyo3::types::{PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo}; |
4 |
| -use speedate::{Date, DateTime, Time}; |
| 3 | +use pyo3::types::{PyDate, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTzInfo}; |
| 4 | +use speedate::{Date, DateTime, Duration, Time}; |
5 | 5 | use strum::EnumMessage;
|
6 | 6 |
|
7 |
| -use super::Input; |
8 | 7 | use crate::errors::{ErrorKind, ValError, ValResult};
|
9 | 8 |
|
| 9 | +use super::Input; |
| 10 | + |
10 | 11 | pub enum EitherDate<'a> {
|
11 | 12 | Raw(Date),
|
12 | 13 | Py(&'a PyDate),
|
@@ -69,6 +70,73 @@ impl<'a> From<&'a PyTime> for EitherTime<'a> {
|
69 | 70 | }
|
70 | 71 | }
|
71 | 72 |
|
| 73 | +#[derive(Debug, Clone)] |
| 74 | +pub enum EitherTimedelta<'a> { |
| 75 | + Raw(Duration), |
| 76 | + Py(&'a PyDelta), |
| 77 | +} |
| 78 | + |
| 79 | +impl<'a> From<Duration> for EitherTimedelta<'a> { |
| 80 | + fn from(timedelta: Duration) -> Self { |
| 81 | + Self::Raw(timedelta) |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +impl<'a> From<&'a PyDelta> for EitherTimedelta<'a> { |
| 86 | + fn from(timedelta: &'a PyDelta) -> Self { |
| 87 | + Self::Py(timedelta) |
| 88 | + } |
| 89 | +} |
| 90 | + |
| 91 | +pub fn pytimedelta_as_timedelta(py_timedelta: &PyDelta) -> Duration { |
| 92 | + // see https://docs.python.org/3/c-api/datetime.html#c.PyDateTime_DELTA_GET_DAYS |
| 93 | + // days can be negative, but seconds and microseconds are always positive. |
| 94 | + let mut days = py_timedelta.get_days(); // -999999999 to 999999999 |
| 95 | + let mut seconds = py_timedelta.get_seconds(); // 0 through 86399 |
| 96 | + let mut microseconds = py_timedelta.get_microseconds(); // 0 through 999999 |
| 97 | + let positive = days >= 0; |
| 98 | + if !positive { |
| 99 | + // negative timedelta, we need to adjust values to match duration logic |
| 100 | + if microseconds != 0 { |
| 101 | + seconds += 1; |
| 102 | + microseconds = (microseconds - 1_000_000).abs(); |
| 103 | + } |
| 104 | + if seconds != 0 { |
| 105 | + days += 1; |
| 106 | + seconds = (seconds - 86400).abs(); |
| 107 | + } |
| 108 | + days = days.abs(); |
| 109 | + } |
| 110 | + // we can safely "unwrap" since the methods above guarantee values are in the correct ranges. |
| 111 | + Duration::new(positive, days as u32, seconds as u32, microseconds as u32).unwrap() |
| 112 | +} |
| 113 | + |
| 114 | +impl<'a> EitherTimedelta<'a> { |
| 115 | + pub fn as_raw(&self) -> Duration { |
| 116 | + match self { |
| 117 | + Self::Raw(timedelta) => timedelta.clone(), |
| 118 | + Self::Py(py_timedelta) => pytimedelta_as_timedelta(py_timedelta), |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + pub fn try_into_py(self, py: Python<'_>) -> PyResult<PyObject> { |
| 123 | + let timedelta = match self { |
| 124 | + Self::Py(timedelta) => Ok(timedelta), |
| 125 | + Self::Raw(duration) => { |
| 126 | + let sign = if duration.positive { 1 } else { -1 }; |
| 127 | + PyDelta::new( |
| 128 | + py, |
| 129 | + sign * duration.day as i32, |
| 130 | + sign * duration.second as i32, |
| 131 | + sign * duration.microsecond as i32, |
| 132 | + true, |
| 133 | + ) |
| 134 | + } |
| 135 | + }?; |
| 136 | + Ok(timedelta.into_py(py)) |
| 137 | + } |
| 138 | +} |
| 139 | + |
72 | 140 | macro_rules! pytime_as_time {
|
73 | 141 | ($py_time:expr) => {
|
74 | 142 | speedate::Time {
|
@@ -287,6 +355,36 @@ pub fn float_as_time<'a>(input: &'a impl Input<'a>, timestamp: f64) -> ValResult
|
287 | 355 | int_as_time(input, timestamp.floor() as i64, microseconds.round() as u32)
|
288 | 356 | }
|
289 | 357 |
|
| 358 | +pub fn bytes_as_timedelta<'a, 'b>(input: &'a impl Input<'a>, bytes: &'b [u8]) -> ValResult<'a, EitherTimedelta<'a>> { |
| 359 | + match Duration::parse_bytes(bytes) { |
| 360 | + Ok(dt) => Ok(dt.into()), |
| 361 | + Err(err) => Err(ValError::new( |
| 362 | + ErrorKind::TimeDeltaParsing { |
| 363 | + error: err.get_documentation().unwrap_or_default(), |
| 364 | + }, |
| 365 | + input, |
| 366 | + )), |
| 367 | + } |
| 368 | +} |
| 369 | + |
| 370 | +pub fn int_as_duration(total_seconds: i64) -> Duration { |
| 371 | + let positive = total_seconds >= 0; |
| 372 | + let total_seconds = total_seconds.unsigned_abs(); |
| 373 | + // we can safely unwrap here since we've guaranteed seconds and microseconds can't cause overflow |
| 374 | + let days = (total_seconds / 86400) as u32; |
| 375 | + let seconds = (total_seconds % 86400) as u32; |
| 376 | + Duration::new(positive, days, seconds, 0).unwrap() |
| 377 | +} |
| 378 | + |
| 379 | +pub fn float_as_duration(total_seconds: f64) -> Duration { |
| 380 | + let positive = total_seconds >= 0_f64; |
| 381 | + let total_seconds = total_seconds.abs(); |
| 382 | + let microsecond = total_seconds.fract() * 1_000_000.0; |
| 383 | + let days = (total_seconds / 86400f64) as u32; |
| 384 | + let seconds = total_seconds as u64 % 86400; |
| 385 | + Duration::new(positive, days, seconds as u32, microsecond.round() as u32).unwrap() |
| 386 | +} |
| 387 | + |
290 | 388 | #[pyclass(module = "pydantic_core._pydantic_core", extends = PyTzInfo)]
|
291 | 389 | #[derive(Debug, Clone)]
|
292 | 390 | struct TzInfo {
|
|
0 commit comments