summaryrefslogtreecommitdiffstats
path: root/rust/src/python
diff options
context:
space:
mode:
Diffstat (limited to 'rust/src/python')
-rw-r--r--rust/src/python/helpers.rs388
-rw-r--r--rust/src/python/mod.rs27
-rw-r--r--rust/src/python/parsing.rs117
-rw-r--r--rust/src/python/types/duration.rs59
-rw-r--r--rust/src/python/types/interval.rs46
-rw-r--r--rust/src/python/types/mod.rs7
-rw-r--r--rust/src/python/types/precise_diff.rs53
-rw-r--r--rust/src/python/types/timezone.rs52
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())
+ }
+}