diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2023-12-17 14:32:20 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2023-12-17 14:32:20 +0000 |
commit | db51f7f103bbbd6c91c8f47d75b3482ef8939691 (patch) | |
tree | ab59b1147bd0cd39f31a48073cff236ede4ec1df /rust/src/python | |
parent | Adding upstream version 3.0.0~a1. (diff) | |
download | pendulum-upstream.tar.xz pendulum-upstream.zip |
Adding upstream version 3.0.0.upstream/3.0.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'rust/src/python')
-rw-r--r-- | rust/src/python/helpers.rs | 388 | ||||
-rw-r--r-- | rust/src/python/mod.rs | 27 | ||||
-rw-r--r-- | rust/src/python/parsing.rs | 117 | ||||
-rw-r--r-- | rust/src/python/types/duration.rs | 59 | ||||
-rw-r--r-- | rust/src/python/types/interval.rs | 46 | ||||
-rw-r--r-- | rust/src/python/types/mod.rs | 7 | ||||
-rw-r--r-- | rust/src/python/types/precise_diff.rs | 53 | ||||
-rw-r--r-- | rust/src/python/types/timezone.rs | 52 |
8 files changed, 749 insertions, 0 deletions
diff --git a/rust/src/python/helpers.rs b/rust/src/python/helpers.rs new file mode 100644 index 0000000..4a53e59 --- /dev/null +++ b/rust/src/python/helpers.rs @@ -0,0 +1,388 @@ +use std::cmp::Ordering; + +use pyo3::{ + intern, + prelude::*, + types::{PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyString, PyTimeAccess}, + PyTypeInfo, +}; + +use crate::{ + constants::{DAYS_PER_MONTHS, SECS_PER_DAY, SECS_PER_HOUR, SECS_PER_MIN}, + helpers, +}; + +use crate::python::types::PreciseDiff; + +struct DateTimeInfo<'py> { + pub year: i32, + pub month: i32, + pub day: i32, + pub hour: i32, + pub minute: i32, + pub second: i32, + pub microsecond: i32, + pub total_seconds: i32, + pub offset: i32, + pub tz: &'py str, + pub is_datetime: bool, +} + +impl PartialEq for DateTimeInfo<'_> { + fn eq(&self, other: &Self) -> bool { + ( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + ) + .eq(&( + other.year, + other.month, + other.day, + other.hour, + other.minute, + other.second, + other.microsecond, + )) + } +} + +impl PartialOrd for DateTimeInfo<'_> { + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + ( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + ) + .partial_cmp(&( + other.year, + other.month, + other.day, + other.hour, + other.minute, + other.second, + other.microsecond, + )) + } +} + +pub fn get_tz_name<'py>(py: Python, dt: &'py PyAny) -> PyResult<&'py str> { + let tz: &str = ""; + + if !PyDateTime::is_type_of(dt) { + return Ok(tz); + } + + let tzinfo = dt.getattr("tzinfo"); + + match tzinfo { + Err(_) => Ok(tz), + Ok(tzinfo) => { + if tzinfo.is_none() { + return Ok(tz); + } + if tzinfo.hasattr(intern!(py, "key")).unwrap_or(false) { + // zoneinfo timezone + let tzname: &PyString = tzinfo + .getattr(intern!(py, "key")) + .unwrap() + .downcast() + .unwrap(); + + return tzname.to_str(); + } else if tzinfo.hasattr(intern!(py, "name")).unwrap_or(false) { + // Pendulum timezone + let tzname: &PyString = tzinfo + .getattr(intern!(py, "name")) + .unwrap() + .downcast() + .unwrap(); + + return tzname.to_str(); + } else if tzinfo.hasattr(intern!(py, "zone")).unwrap_or(false) { + // pytz timezone + let tzname: &PyString = tzinfo + .getattr(intern!(py, "zone")) + .unwrap() + .downcast() + .unwrap(); + + return tzname.to_str(); + } + + Ok(tz) + } + } +} + +pub fn get_offset(dt: &PyAny) -> PyResult<i32> { + if !PyDateTime::is_type_of(dt) { + return Ok(0); + } + + let tzinfo = dt.getattr("tzinfo")?; + + if tzinfo.is_none() { + return Ok(0); + } + + let offset: &PyDelta = tzinfo.call_method1("utcoffset", (dt,))?.downcast()?; + + Ok(offset.get_days() * SECS_PER_DAY as i32 + offset.get_seconds()) +} + +#[pyfunction] +pub fn is_leap(year: i32) -> PyResult<bool> { + Ok(helpers::is_leap(year)) +} + +#[pyfunction] +pub fn is_long_year(year: i32) -> PyResult<bool> { + Ok(helpers::is_long_year(year)) +} + +#[pyfunction] +pub fn week_day(year: i32, month: u32, day: u32) -> PyResult<u32> { + Ok(helpers::week_day(year, month, day)) +} + +#[pyfunction] +pub fn days_in_year(year: i32) -> PyResult<u32> { + Ok(helpers::days_in_year(year)) +} + +#[pyfunction] +pub fn local_time( + unix_time: f64, + utc_offset: isize, + microsecond: usize, +) -> PyResult<(usize, usize, usize, usize, usize, usize, usize)> { + Ok(helpers::local_time(unix_time, utc_offset, microsecond)) +} + +#[pyfunction] +pub fn precise_diff<'py>(py: Python, dt1: &'py PyAny, dt2: &'py PyAny) -> PyResult<PreciseDiff> { + let mut sign = 1; + let mut dtinfo1 = DateTimeInfo { + year: dt1.downcast::<PyDate>()?.get_year(), + month: i32::from(dt1.downcast::<PyDate>()?.get_month()), + day: i32::from(dt1.downcast::<PyDate>()?.get_day()), + hour: 0, + minute: 0, + second: 0, + microsecond: 0, + total_seconds: 0, + tz: get_tz_name(py, dt1)?, + offset: get_offset(dt1)?, + is_datetime: PyDateTime::is_type_of(dt1), + }; + let mut dtinfo2 = DateTimeInfo { + year: dt2.downcast::<PyDate>()?.get_year(), + month: i32::from(dt2.downcast::<PyDate>()?.get_month()), + day: i32::from(dt2.downcast::<PyDate>()?.get_day()), + hour: 0, + minute: 0, + second: 0, + microsecond: 0, + total_seconds: 0, + tz: get_tz_name(py, dt2)?, + offset: get_offset(dt2)?, + is_datetime: PyDateTime::is_type_of(dt2), + }; + let in_same_tz: bool = dtinfo1.tz == dtinfo2.tz && !dtinfo1.tz.is_empty(); + let mut total_days = helpers::day_number(dtinfo2.year, dtinfo2.month as u8, dtinfo2.day as u8) + - helpers::day_number(dtinfo1.year, dtinfo1.month as u8, dtinfo1.day as u8); + + if dtinfo1.is_datetime { + let dt1dt: &PyDateTime = dt1.downcast()?; + + dtinfo1.hour = i32::from(dt1dt.get_hour()); + dtinfo1.minute = i32::from(dt1dt.get_minute()); + dtinfo1.second = i32::from(dt1dt.get_second()); + dtinfo1.microsecond = dt1dt.get_microsecond() as i32; + + if !in_same_tz && dtinfo1.offset != 0 || total_days == 0 { + dtinfo1.hour -= dtinfo1.offset / SECS_PER_HOUR as i32; + dtinfo1.offset %= SECS_PER_HOUR as i32; + dtinfo1.minute -= dtinfo1.offset / SECS_PER_MIN as i32; + dtinfo1.offset %= SECS_PER_MIN as i32; + dtinfo1.second -= dtinfo1.offset; + + if dtinfo1.second < 0 { + dtinfo1.second += 60; + dtinfo1.minute -= 1; + } else if dtinfo1.second > 60 { + dtinfo1.second -= 60; + dtinfo1.minute += 1; + } + + if dtinfo1.minute < 0 { + dtinfo1.minute += 60; + dtinfo1.hour -= 1; + } else if dtinfo1.minute > 60 { + dtinfo1.minute -= 60; + dtinfo1.hour += 1; + } + + if dtinfo1.hour < 0 { + dtinfo1.hour += 24; + dtinfo1.day -= 1; + } else if dtinfo1.hour > 24 { + dtinfo1.hour -= 24; + dtinfo1.day += 1; + } + } + + dtinfo1.total_seconds = dtinfo1.hour * SECS_PER_HOUR as i32 + + dtinfo1.minute * SECS_PER_MIN as i32 + + dtinfo1.second; + } + + if dtinfo2.is_datetime { + let dt2dt: &PyDateTime = dt2.downcast()?; + + dtinfo2.hour = i32::from(dt2dt.get_hour()); + dtinfo2.minute = i32::from(dt2dt.get_minute()); + dtinfo2.second = i32::from(dt2dt.get_second()); + dtinfo2.microsecond = dt2dt.get_microsecond() as i32; + + if !in_same_tz && dtinfo2.offset != 0 || total_days == 0 { + dtinfo2.hour -= dtinfo2.offset / SECS_PER_HOUR as i32; + dtinfo2.offset %= SECS_PER_HOUR as i32; + dtinfo2.minute -= dtinfo2.offset / SECS_PER_MIN as i32; + dtinfo2.offset %= SECS_PER_MIN as i32; + dtinfo2.second -= dtinfo2.offset; + + if dtinfo2.second < 0 { + dtinfo2.second += 60; + dtinfo2.minute -= 1; + } else if dtinfo2.second > 60 { + dtinfo2.second -= 60; + dtinfo2.minute += 1; + } + + if dtinfo2.minute < 0 { + dtinfo2.minute += 60; + dtinfo2.hour -= 1; + } else if dtinfo2.minute > 60 { + dtinfo2.minute -= 60; + dtinfo2.hour += 1; + } + + if dtinfo2.hour < 0 { + dtinfo2.hour += 24; + dtinfo2.day -= 1; + } else if dtinfo2.hour > 24 { + dtinfo2.hour -= 24; + dtinfo2.day += 1; + } + } + + dtinfo2.total_seconds = dtinfo2.hour * SECS_PER_HOUR as i32 + + dtinfo2.minute * SECS_PER_MIN as i32 + + dtinfo2.second; + } + + if dtinfo1 > dtinfo2 { + sign = -1; + (dtinfo1, dtinfo2) = (dtinfo2, dtinfo1); + + total_days = -total_days; + } + + let mut year_diff = dtinfo2.year - dtinfo1.year; + let mut month_diff = dtinfo2.month - dtinfo1.month; + let mut day_diff = dtinfo2.day - dtinfo1.day; + let mut hour_diff = dtinfo2.hour - dtinfo1.hour; + let mut minute_diff = dtinfo2.minute - dtinfo1.minute; + let mut second_diff = dtinfo2.second - dtinfo1.second; + let mut microsecond_diff = dtinfo2.microsecond - dtinfo1.microsecond; + + if microsecond_diff < 0 { + microsecond_diff += 1_000_000; + second_diff -= 1; + } + + if second_diff < 0 { + second_diff += 60; + minute_diff -= 1; + } + + if minute_diff < 0 { + minute_diff += 60; + hour_diff -= 1; + } + + if hour_diff < 0 { + hour_diff += 24; + day_diff -= 1; + } + + if day_diff < 0 { + // If we have a difference in days, + // we have to check if they represent months + let mut year = dtinfo2.year; + let mut month = dtinfo2.month; + + if month == 1 { + month = 12; + year -= 1; + } else { + month -= 1; + } + + let leap = helpers::is_leap(year); + + let days_in_last_month = DAYS_PER_MONTHS[usize::from(leap)][month as usize]; + let days_in_month = + DAYS_PER_MONTHS[usize::from(helpers::is_leap(dtinfo2.year))][dtinfo2.month as usize]; + + match day_diff.cmp(&(days_in_month - days_in_last_month)) { + Ordering::Less => { + // We don't have a full month, we calculate days + if days_in_last_month < dtinfo1.day { + day_diff += dtinfo1.day; + } else { + day_diff += days_in_last_month; + } + } + Ordering::Equal => { + // We have exactly a full month + // We remove the days difference + // and add one to the months difference + day_diff = 0; + month_diff += 1; + } + Ordering::Greater => { + // We have a full month + day_diff += days_in_last_month; + } + } + + month_diff -= 1; + } + + if month_diff < 0 { + month_diff += 12; + year_diff -= 1; + } + + Ok(PreciseDiff { + years: year_diff * sign, + months: month_diff * sign, + days: day_diff * sign, + hours: hour_diff * sign, + minutes: minute_diff * sign, + seconds: second_diff * sign, + microseconds: microsecond_diff * sign, + total_days: total_days * sign, + }) +} diff --git a/rust/src/python/mod.rs b/rust/src/python/mod.rs new file mode 100644 index 0000000..8d3cd41 --- /dev/null +++ b/rust/src/python/mod.rs @@ -0,0 +1,27 @@ +use pyo3::prelude::*; + +mod helpers; +mod parsing; +mod types; + +use helpers::{days_in_year, is_leap, is_long_year, local_time, precise_diff, week_day}; +use parsing::parse_iso8601; +use types::{Duration, PreciseDiff}; + +#[pymodule] +pub fn _pendulum(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(days_in_year, m)?)?; + m.add_function(wrap_pyfunction!(is_leap, m)?)?; + m.add_function(wrap_pyfunction!(is_long_year, m)?)?; + m.add_function(wrap_pyfunction!(local_time, m)?)?; + m.add_function(wrap_pyfunction!(week_day, m)?)?; + m.add_function(wrap_pyfunction!(parse_iso8601, m)?)?; + m.add_function(wrap_pyfunction!(precise_diff, m)?)?; + m.add_class::<Duration>()?; + m.add_class::<PreciseDiff>()?; + + #[cfg(not(feature = "mimalloc"))] + m.setattr("__pendulum_default_allocator__", true)?; // uses setattr so this is not in __all__ + + Ok(()) +} diff --git a/rust/src/python/parsing.rs b/rust/src/python/parsing.rs new file mode 100644 index 0000000..48fa64c --- /dev/null +++ b/rust/src/python/parsing.rs @@ -0,0 +1,117 @@ +use pyo3::exceptions; +use pyo3::prelude::*; +use pyo3::types::PyDate; +use pyo3::types::PyDateTime; +use pyo3::types::PyTime; + +use crate::parsing::Parser; +use crate::python::types::{Duration, FixedTimezone}; + +#[pyfunction] +pub fn parse_iso8601(py: Python, input: &str) -> PyResult<PyObject> { + let parsed = Parser::new(input).parse(); + + match parsed { + Ok(parsed) => match (parsed.datetime, parsed.duration, parsed.second_datetime) { + (Some(datetime), None, None) => match (datetime.has_date, datetime.has_time) { + (true, true) => match datetime.offset { + Some(offset) => { + let dt = PyDateTime::new( + py, + datetime.year as i32, + datetime.month as u8, + datetime.day as u8, + datetime.hour as u8, + datetime.minute as u8, + datetime.second as u8, + datetime.microsecond, + Some( + Py::new(py, FixedTimezone::new(offset, datetime.tzname))? + .to_object(py) + .extract(py)?, + ), + )?; + + Ok(dt.to_object(py)) + } + None => { + let dt = PyDateTime::new( + py, + datetime.year as i32, + datetime.month as u8, + datetime.day as u8, + datetime.hour as u8, + datetime.minute as u8, + datetime.second as u8, + datetime.microsecond, + None, + )?; + + Ok(dt.to_object(py)) + } + }, + (true, false) => { + let dt = PyDate::new( + py, + datetime.year as i32, + datetime.month as u8, + datetime.day as u8, + )?; + + Ok(dt.to_object(py)) + } + (false, true) => match datetime.offset { + Some(offset) => { + let dt = PyTime::new( + py, + datetime.hour as u8, + datetime.minute as u8, + datetime.second as u8, + datetime.microsecond, + Some( + Py::new(py, FixedTimezone::new(offset, datetime.tzname))? + .to_object(py) + .extract(py)?, + ), + )?; + + Ok(dt.to_object(py)) + } + None => { + let dt = PyTime::new( + py, + datetime.hour as u8, + datetime.minute as u8, + datetime.second as u8, + datetime.microsecond, + None, + )?; + + Ok(dt.to_object(py)) + } + }, + (_, _) => Err(exceptions::PyValueError::new_err( + "Parsing error".to_string(), + )), + }, + (None, Some(duration), None) => Ok(Py::new( + py, + Duration::new( + Some(duration.years), + Some(duration.months), + Some(duration.weeks), + Some(duration.days), + Some(duration.hours), + Some(duration.minutes), + Some(duration.seconds), + Some(duration.microseconds), + ), + )? + .to_object(py)), + (_, _, _) => Err(exceptions::PyValueError::new_err( + "Not yet implemented".to_string(), + )), + }, + Err(error) => Err(exceptions::PyValueError::new_err(error.to_string())), + } +} diff --git a/rust/src/python/types/duration.rs b/rust/src/python/types/duration.rs new file mode 100644 index 0000000..fc18f4e --- /dev/null +++ b/rust/src/python/types/duration.rs @@ -0,0 +1,59 @@ +use pyo3::prelude::*; + +#[pyclass(module = "_pendulum")] +pub struct Duration { + #[pyo3(get, set)] + pub years: u32, + #[pyo3(get, set)] + pub months: u32, + #[pyo3(get, set)] + pub weeks: u32, + #[pyo3(get, set)] + pub days: u32, + #[pyo3(get, set)] + pub hours: u32, + #[pyo3(get, set)] + pub minutes: u32, + #[pyo3(get, set)] + pub seconds: u32, + #[pyo3(get, set)] + pub microseconds: u32, +} + +#[pymethods] +impl Duration { + #[new] + #[pyo3(signature = (years=0, months=0, weeks=0, days=0, hours=0, minutes=0, seconds=0, microseconds=0))] + #[allow(clippy::too_many_arguments)] + pub fn new( + years: Option<u32>, + months: Option<u32>, + weeks: Option<u32>, + days: Option<u32>, + hours: Option<u32>, + minutes: Option<u32>, + seconds: Option<u32>, + microseconds: Option<u32>, + ) -> Self { + Self { + years: years.unwrap_or(0), + months: months.unwrap_or(0), + weeks: weeks.unwrap_or(0), + days: days.unwrap_or(0), + hours: hours.unwrap_or(0), + minutes: minutes.unwrap_or(0), + seconds: seconds.unwrap_or(0), + microseconds: microseconds.unwrap_or(0), + } + } + + #[getter] + fn remaining_days(&self) -> PyResult<u32> { + Ok(self.days) + } + + #[getter] + fn remaining_seconds(&self) -> PyResult<u32> { + Ok(self.seconds) + } +} diff --git a/rust/src/python/types/interval.rs b/rust/src/python/types/interval.rs new file mode 100644 index 0000000..7137493 --- /dev/null +++ b/rust/src/python/types/interval.rs @@ -0,0 +1,46 @@ +use pyo3::prelude::*; + +use pyo3::types::PyDelta; + +#[pyclass(extends=PyDelta)] +#[derive(Default)] +pub struct Interval { + pub days: i32, + pub seconds: i32, + pub microseconds: i32, +} + +#[pymethods] +impl Interval { + #[new] + #[pyo3(signature = (days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0))] + pub fn new( + py: Python, + days: Option<i32>, + seconds: Option<i32>, + microseconds: Option<i32>, + milliseconds: Option<i32>, + minutes: Option<i32>, + hours: Option<i32>, + weeks: Option<i32>, + ) -> PyResult<Self> { + println!("{} days", 31); + PyDelta::new( + py, + days.unwrap_or(0), + seconds.unwrap_or(0), + microseconds.unwrap_or(0), + true, + )?; + + let f = Ok(Self { + days: days.unwrap_or(0), + seconds: seconds.unwrap_or(0), + microseconds: microseconds.unwrap_or(0), + }); + + println!("{} days", 31); + + f + } +} diff --git a/rust/src/python/types/mod.rs b/rust/src/python/types/mod.rs new file mode 100644 index 0000000..cba11df --- /dev/null +++ b/rust/src/python/types/mod.rs @@ -0,0 +1,7 @@ +mod duration; +mod precise_diff; +mod timezone; + +pub use duration::Duration; +pub use precise_diff::PreciseDiff; +pub use timezone::FixedTimezone; diff --git a/rust/src/python/types/precise_diff.rs b/rust/src/python/types/precise_diff.rs new file mode 100644 index 0000000..64ca3a6 --- /dev/null +++ b/rust/src/python/types/precise_diff.rs @@ -0,0 +1,53 @@ +use pyo3::prelude::*; + +#[pyclass(module = "_pendulum")] +pub struct PreciseDiff { + #[pyo3(get, set)] + pub years: i32, + #[pyo3(get, set)] + pub months: i32, + #[pyo3(get, set)] + pub days: i32, + #[pyo3(get, set)] + pub hours: i32, + #[pyo3(get, set)] + pub minutes: i32, + #[pyo3(get, set)] + pub seconds: i32, + #[pyo3(get, set)] + pub microseconds: i32, + #[pyo3(get, set)] + pub total_days: i32, +} + +#[pymethods] +impl PreciseDiff { + #[new] + #[pyo3(signature = (years=0, months=0, days=0, hours=0, minutes=0, seconds=0, microseconds=0, total_days=0))] + #[allow(clippy::too_many_arguments)] + pub fn new( + years: Option<i32>, + months: Option<i32>, + days: Option<i32>, + hours: Option<i32>, + minutes: Option<i32>, + seconds: Option<i32>, + microseconds: Option<i32>, + total_days: Option<i32>, + ) -> Self { + Self { + years: years.unwrap_or(0), + months: months.unwrap_or(0), + days: days.unwrap_or(0), + hours: hours.unwrap_or(0), + minutes: minutes.unwrap_or(0), + seconds: seconds.unwrap_or(0), + microseconds: microseconds.unwrap_or(0), + total_days: total_days.unwrap_or(0), + } + } + + fn __repr__(&self) -> String { + format!("PreciseDiff(years={}, months={}, days={}, hours={}, minutes={}, seconds={}, microseconds={}, total_days={})", self.years, self.months, self.days, self.hours, self.minutes, self.seconds, self.microseconds, self.total_days) + } +} diff --git a/rust/src/python/types/timezone.rs b/rust/src/python/types/timezone.rs new file mode 100644 index 0000000..1a8bbad --- /dev/null +++ b/rust/src/python/types/timezone.rs @@ -0,0 +1,52 @@ +use pyo3::prelude::*; +use pyo3::types::{PyDelta, PyDict, PyTzInfo}; + +#[pyclass(module = "_pendulum", extends = PyTzInfo)] +#[derive(Clone)] +pub struct FixedTimezone { + offset: i32, + name: Option<String>, +} + +#[pymethods] +impl FixedTimezone { + #[new] + pub fn new(offset: i32, name: Option<String>) -> Self { + Self { offset, name } + } + + fn utcoffset<'p>(&self, py: Python<'p>, _dt: &PyAny) -> PyResult<&'p PyDelta> { + PyDelta::new(py, 0, self.offset, 0, true) + } + + fn tzname(&self, _dt: &PyAny) -> String { + self.__str__() + } + + fn dst<'p>(&self, py: Python<'p>, _dt: &PyAny) -> PyResult<&'p PyDelta> { + PyDelta::new(py, 0, 0, 0, true) + } + + fn __repr__(&self) -> String { + format!( + "FixedTimezone({}, name=\"{}\")", + self.offset, + self.__str__() + ) + } + + fn __str__(&self) -> String { + if let Some(n) = &self.name { + n.clone() + } else { + let sign = if self.offset < 0 { "-" } else { "+" }; + let minutes = self.offset.abs() / 60; + let (hour, minute) = (minutes / 60, minutes % 60); + format!("{sign}{hour:.2}:{minute:.2}") + } + } + + fn __deepcopy__(&self, py: Python, _memo: &PyDict) -> PyResult<Py<Self>> { + Py::new(py, self.clone()) + } +} |