#![cfg(feature = "python")]
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyType;
use crate::{Date, DateTime, Duration, OffsetDateTime, Time, UtcOffset, Weekday as RustWeekday};
#[pyclass(name = "Weekday", module = "fasttime")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PyWeekday(RustWeekday);
#[pymethods]
impl PyWeekday {
#[classattr]
const MONDAY: PyWeekday = PyWeekday(RustWeekday::Monday);
#[classattr]
const TUESDAY: PyWeekday = PyWeekday(RustWeekday::Tuesday);
#[classattr]
const WEDNESDAY: PyWeekday = PyWeekday(RustWeekday::Wednesday);
#[classattr]
const THURSDAY: PyWeekday = PyWeekday(RustWeekday::Thursday);
#[classattr]
const FRIDAY: PyWeekday = PyWeekday(RustWeekday::Friday);
#[classattr]
const SATURDAY: PyWeekday = PyWeekday(RustWeekday::Saturday);
#[classattr]
const SUNDAY: PyWeekday = PyWeekday(RustWeekday::Sunday);
#[pyo3(name = "number_from_monday")]
fn number_from_monday(&self) -> u8 {
self.0.number_from_monday()
}
fn __repr__(&self) -> String {
format!("{:?}", self.0)
}
fn __str__(&self) -> String {
format!("{:?}", self.0)
}
}
#[pyclass(name = "Date", module = "fasttime")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PyDate(Date);
#[pymethods]
impl PyDate {
#[new]
fn new(year: i32, month: u8, day: u8) -> PyResult<Self> {
Date::from_ymd(year, month, day)
.map(PyDate)
.map_err(|e| PyValueError::new_err(format!("Invalid date: {:?}", e)))
}
#[getter]
fn year(&self) -> i32 {
self.0.year
}
#[getter]
fn month(&self) -> u8 {
self.0.month
}
#[getter]
fn day(&self) -> u8 {
self.0.day
}
#[classmethod]
#[pyo3(name = "from_days_since_unix_epoch")]
fn from_days_since_unix_epoch(_cls: &Bound<'_, PyType>, days: i64) -> PyResult<Self> {
Date::from_days_since_unix_epoch(days)
.map(PyDate)
.map_err(|e| PyValueError::new_err(format!("Invalid date: {:?}", e)))
}
#[pyo3(name = "days_since_unix_epoch")]
fn days_since_unix_epoch(&self) -> i64 {
self.0.days_since_unix_epoch()
}
#[pyo3(name = "weekday")]
fn weekday(&self) -> PyWeekday {
PyWeekday(self.0.weekday())
}
#[pyo3(name = "ordinal")]
fn ordinal(&self) -> u16 {
self.0.ordinal()
}
#[pyo3(name = "add_days")]
fn add_days(&self, days: i64) -> PyResult<Self> {
self.0
.add_days(days)
.map(PyDate)
.map_err(|e| PyValueError::new_err(format!("Date out of range: {:?}", e)))
}
#[classmethod]
#[pyo3(name = "parse")]
fn parse(_cls: &Bound<'_, PyType>, s: &str) -> PyResult<Self> {
s.parse::<Date>()
.map(PyDate)
.map_err(|e| PyValueError::new_err(format!("Invalid date string: {:?}", e)))
}
fn __str__(&self) -> String {
self.0.to_string()
}
fn __repr__(&self) -> String {
format!(
"Date(year={}, month={}, day={})",
self.0.year, self.0.month, self.0.day
)
}
fn __richcmp__(&self, other: &Self, op: pyo3::basic::CompareOp) -> PyResult<bool> {
use pyo3::basic::CompareOp;
match op {
CompareOp::Lt => Ok(self.0 < other.0),
CompareOp::Le => Ok(self.0 <= other.0),
CompareOp::Eq => Ok(self.0 == other.0),
CompareOp::Ne => Ok(self.0 != other.0),
CompareOp::Gt => Ok(self.0 > other.0),
CompareOp::Ge => Ok(self.0 >= other.0),
}
}
fn __hash__(&self) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
self.0.hash(&mut hasher);
hasher.finish()
}
}
#[pyclass(name = "Time", module = "fasttime")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PyTime(Time);
#[pymethods]
impl PyTime {
#[new]
#[pyo3(signature = (hour, minute, second, nanosecond=0))]
fn new(hour: u8, minute: u8, second: u8, nanosecond: u32) -> PyResult<Self> {
Time::from_hms_nano(hour, minute, second, nanosecond)
.map(PyTime)
.map_err(|e| PyValueError::new_err(format!("Invalid time: {:?}", e)))
}
#[getter]
fn hour(&self) -> u8 {
self.0.hour
}
#[getter]
fn minute(&self) -> u8 {
self.0.minute
}
#[getter]
fn second(&self) -> u8 {
self.0.second
}
#[getter]
fn nanosecond(&self) -> u32 {
self.0.nanosecond
}
#[pyo3(name = "seconds_since_midnight")]
fn seconds_since_midnight(&self) -> u32 {
self.0.seconds_since_midnight()
}
#[pyo3(name = "nanos_since_midnight")]
fn nanos_since_midnight(&self) -> u64 {
self.0.nanos_since_midnight()
}
#[classmethod]
#[pyo3(name = "parse")]
fn parse(_cls: &Bound<'_, PyType>, s: &str) -> PyResult<Self> {
s.parse::<Time>()
.map(PyTime)
.map_err(|e| PyValueError::new_err(format!("Invalid time string: {:?}", e)))
}
fn __str__(&self) -> String {
self.0.to_string()
}
fn __repr__(&self) -> String {
format!(
"Time(hour={}, minute={}, second={}, nanosecond={})",
self.0.hour, self.0.minute, self.0.second, self.0.nanosecond
)
}
fn __richcmp__(&self, other: &Self, op: pyo3::basic::CompareOp) -> PyResult<bool> {
use pyo3::basic::CompareOp;
match op {
CompareOp::Lt => Ok(self.0 < other.0),
CompareOp::Le => Ok(self.0 <= other.0),
CompareOp::Eq => Ok(self.0 == other.0),
CompareOp::Ne => Ok(self.0 != other.0),
CompareOp::Gt => Ok(self.0 > other.0),
CompareOp::Ge => Ok(self.0 >= other.0),
}
}
fn __hash__(&self) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
self.0.hash(&mut hasher);
hasher.finish()
}
}
#[pyclass(name = "Duration", module = "fasttime")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PyDuration(Duration);
#[pymethods]
impl PyDuration {
#[classmethod]
#[pyo3(name = "seconds")]
fn seconds(_cls: &Bound<'_, PyType>, secs: i64) -> Self {
PyDuration(Duration::seconds(secs))
}
#[classmethod]
#[pyo3(name = "milliseconds")]
fn milliseconds(_cls: &Bound<'_, PyType>, ms: i64) -> Self {
PyDuration(Duration::milliseconds(ms))
}
#[classmethod]
#[pyo3(name = "microseconds")]
fn microseconds(_cls: &Bound<'_, PyType>, us: i64) -> Self {
PyDuration(Duration::microseconds(us))
}
#[classmethod]
#[pyo3(name = "nanoseconds")]
fn nanoseconds(_cls: &Bound<'_, PyType>, ns: i128) -> Self {
PyDuration(Duration::nanoseconds(ns))
}
#[pyo3(name = "total_seconds")]
fn total_seconds(&self) -> f64 {
self.0.total_seconds()
}
#[pyo3(name = "total_nanos")]
fn total_nanos(&self) -> i128 {
self.0.total_nanos()
}
fn __add__(&self, other: &Self) -> Self {
PyDuration(self.0 + other.0)
}
fn __sub__(&self, other: &Self) -> Self {
PyDuration(self.0 - other.0)
}
fn __neg__(&self) -> Self {
PyDuration(-self.0)
}
fn __str__(&self) -> String {
format!("Duration({} ns)", self.0.total_nanos())
}
fn __repr__(&self) -> String {
format!("Duration.nanoseconds({})", self.0.total_nanos())
}
fn __richcmp__(&self, other: &Self, op: pyo3::basic::CompareOp) -> PyResult<bool> {
use pyo3::basic::CompareOp;
match op {
CompareOp::Lt => Ok(self.0 < other.0),
CompareOp::Le => Ok(self.0 <= other.0),
CompareOp::Eq => Ok(self.0 == other.0),
CompareOp::Ne => Ok(self.0 != other.0),
CompareOp::Gt => Ok(self.0 > other.0),
CompareOp::Ge => Ok(self.0 >= other.0),
}
}
fn __hash__(&self) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
self.0.hash(&mut hasher);
hasher.finish()
}
}
#[pyclass(name = "DateTime", module = "fasttime")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PyDateTime(DateTime);
#[pymethods]
impl PyDateTime {
#[new]
fn new(date: &PyDate, time: &PyTime) -> Self {
PyDateTime(DateTime::new(date.0, time.0))
}
#[getter]
fn date(&self) -> PyDate {
PyDate(self.0.date)
}
#[getter]
fn time(&self) -> PyTime {
PyTime(self.0.time)
}
#[classmethod]
#[pyo3(name = "from_unix_timestamp", signature = (secs, nanos=0))]
fn from_unix_timestamp(_cls: &Bound<'_, PyType>, secs: i64, nanos: i32) -> PyResult<Self> {
DateTime::from_unix_timestamp(secs, nanos)
.map(PyDateTime)
.map_err(|e| PyValueError::new_err(format!("Invalid timestamp: {:?}", e)))
}
#[pyo3(name = "unix_timestamp")]
fn unix_timestamp(&self) -> i64 {
self.0.unix_timestamp()
}
#[pyo3(name = "unix_timestamp_nanos")]
fn unix_timestamp_nanos(&self) -> i128 {
self.0.unix_timestamp_nanos()
}
#[pyo3(name = "add_duration")]
fn add_duration(&self, dur: &PyDuration) -> PyResult<Self> {
self.0
.add_duration(dur.0)
.map(PyDateTime)
.map_err(|e| PyValueError::new_err(format!("DateTime out of range: {:?}", e)))
}
#[pyo3(name = "difference")]
fn difference(&self, other: &PyDateTime) -> PyDuration {
PyDuration(self.0.difference(other.0))
}
#[classmethod]
#[pyo3(name = "now_utc")]
fn now_utc(_cls: &Bound<'_, PyType>) -> PyResult<Self> {
#[cfg(feature = "std")]
{
DateTime::now_utc()
.map(PyDateTime)
.map_err(|e| PyValueError::new_err(format!("Failed to get current time: {:?}", e)))
}
#[cfg(not(feature = "std"))]
{
Err(PyValueError::new_err(
"now_utc() requires the 'std' feature",
))
}
}
#[classmethod]
#[pyo3(name = "parse")]
fn parse(_cls: &Bound<'_, PyType>, s: &str) -> PyResult<Self> {
s.parse::<DateTime>().map(PyDateTime).map_err(|_| {
PyValueError::new_err(format!(
"Invalid datetime string '{}'. Expected format: YYYY-MM-DDTHH:MM:SS[.fffffffff]Z",
s
))
})
}
fn __str__(&self) -> String {
self.0.to_string()
}
fn __repr__(&self) -> String {
format!("DateTime.parse('{}')", self.0)
}
fn __richcmp__(&self, other: &Self, op: pyo3::basic::CompareOp) -> PyResult<bool> {
use pyo3::basic::CompareOp;
match op {
CompareOp::Lt => Ok(self.0 < other.0),
CompareOp::Le => Ok(self.0 <= other.0),
CompareOp::Eq => Ok(self.0 == other.0),
CompareOp::Ne => Ok(self.0 != other.0),
CompareOp::Gt => Ok(self.0 > other.0),
CompareOp::Ge => Ok(self.0 >= other.0),
}
}
fn __hash__(&self) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
self.0.hash(&mut hasher);
hasher.finish()
}
}
#[pyclass(name = "UtcOffset", module = "fasttime")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PyUtcOffset(UtcOffset);
#[pymethods]
impl PyUtcOffset {
#[classmethod]
#[pyo3(name = "from_seconds")]
fn from_seconds(_cls: &Bound<'_, PyType>, seconds: i32) -> PyResult<Self> {
UtcOffset::from_seconds(seconds)
.map(PyUtcOffset)
.map_err(|e| PyValueError::new_err(format!("Invalid offset: {:?}", e)))
}
#[classmethod]
#[pyo3(name = "from_hours_minutes")]
fn from_hours_minutes(
_cls: &Bound<'_, PyType>,
sign_positive: bool,
hours: u8,
minutes: u8,
) -> PyResult<Self> {
UtcOffset::from_hours_minutes(sign_positive, hours, minutes)
.map(PyUtcOffset)
.map_err(|e| PyValueError::new_err(format!("Invalid offset: {:?}", e)))
}
#[pyo3(name = "as_seconds")]
fn as_seconds(&self) -> i32 {
self.0.as_seconds()
}
#[pyo3(name = "is_utc")]
fn is_utc(&self) -> bool {
self.0.is_utc()
}
fn __str__(&self) -> String {
self.0.to_string()
}
fn __repr__(&self) -> String {
format!("UtcOffset.from_seconds({})", self.0.as_seconds())
}
fn __richcmp__(&self, other: &Self, op: pyo3::basic::CompareOp) -> PyResult<bool> {
use pyo3::basic::CompareOp;
match op {
CompareOp::Lt => Ok(self.0 < other.0),
CompareOp::Le => Ok(self.0 <= other.0),
CompareOp::Eq => Ok(self.0 == other.0),
CompareOp::Ne => Ok(self.0 != other.0),
CompareOp::Gt => Ok(self.0 > other.0),
CompareOp::Ge => Ok(self.0 >= other.0),
}
}
fn __hash__(&self) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
self.0.hash(&mut hasher);
hasher.finish()
}
}
#[pyclass(name = "OffsetDateTime", module = "fasttime")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PyOffsetDateTime(OffsetDateTime);
#[pymethods]
impl PyOffsetDateTime {
#[classmethod]
#[pyo3(name = "from_utc")]
fn from_utc(_cls: &Bound<'_, PyType>, utc: &PyDateTime, offset: &PyUtcOffset) -> Self {
PyOffsetDateTime(OffsetDateTime::from_utc(utc.0, offset.0))
}
#[classmethod]
#[pyo3(name = "from_local")]
fn from_local(
_cls: &Bound<'_, PyType>,
date: &PyDate,
time: &PyTime,
offset: &PyUtcOffset,
) -> PyResult<Self> {
OffsetDateTime::from_local(date.0, time.0, offset.0)
.map(PyOffsetDateTime)
.map_err(|e| PyValueError::new_err(format!("Invalid local datetime: {:?}", e)))
}
#[getter]
fn utc(&self) -> PyDateTime {
PyDateTime(self.0.utc)
}
#[getter]
fn offset(&self) -> PyUtcOffset {
PyUtcOffset(self.0.offset)
}
#[pyo3(name = "to_local")]
fn to_local(&self) -> PyResult<PyDateTime> {
self.0
.to_local()
.map(PyDateTime)
.map_err(|e| PyValueError::new_err(format!("Local datetime out of range: {:?}", e)))
}
#[pyo3(name = "unix_timestamp")]
fn unix_timestamp(&self) -> i64 {
self.0.unix_timestamp()
}
#[pyo3(name = "unix_timestamp_nanos")]
fn unix_timestamp_nanos(&self) -> i128 {
self.0.unix_timestamp_nanos()
}
#[pyo3(name = "add_duration")]
fn add_duration(&self, dur: &PyDuration) -> PyResult<Self> {
self.0
.add_duration(dur.0)
.map(PyOffsetDateTime)
.map_err(|e| PyValueError::new_err(format!("DateTime out of range: {:?}", e)))
}
#[pyo3(name = "difference")]
fn difference(&self, other: &PyOffsetDateTime) -> PyDuration {
PyDuration(self.0.difference(other.0))
}
#[classmethod]
#[pyo3(name = "parse")]
fn parse(_cls: &Bound<'_, PyType>, s: &str) -> PyResult<Self> {
s.parse::<OffsetDateTime>()
.map(PyOffsetDateTime)
.map_err(|_| {
PyValueError::new_err(format!(
"Invalid offset datetime string '{}'. Expected RFC 3339 format: YYYY-MM-DDTHH:MM:SS[.fffffffff][Z|±HH:MM]",
s
))
})
}
fn __str__(&self) -> String {
self.0.to_string()
}
fn __repr__(&self) -> String {
format!("OffsetDateTime.parse('{}')", self.0)
}
fn __richcmp__(&self, other: &Self, op: pyo3::basic::CompareOp) -> PyResult<bool> {
use pyo3::basic::CompareOp;
match op {
CompareOp::Lt => Ok(self.0 < other.0),
CompareOp::Le => Ok(self.0 <= other.0),
CompareOp::Eq => Ok(self.0 == other.0),
CompareOp::Ne => Ok(self.0 != other.0),
CompareOp::Gt => Ok(self.0 > other.0),
CompareOp::Ge => Ok(self.0 >= other.0),
}
}
fn __hash__(&self) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
self.0.hash(&mut hasher);
hasher.finish()
}
}
#[pymodule]
fn fasttime(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyWeekday>()?;
m.add_class::<PyDate>()?;
m.add_class::<PyTime>()?;
m.add_class::<PyDuration>()?;
m.add_class::<PyDateTime>()?;
m.add_class::<PyUtcOffset>()?;
m.add_class::<PyOffsetDateTime>()?;
Ok(())
}