use pyo3::prelude::*;
use pyo3::Py;
use tempoch::qtty::{unit::JulianYear, Day};
use tempoch::{JulianDate, MJD, TT, UTC};
use crate::errors::{ensure_finite, map_conversion_error};
use crate::interop;
use crate::mjd::PyModifiedJulianDate;
#[pyclass(name = "JulianDate", module = "tempoch", from_py_object)]
#[derive(Clone, Copy)]
pub struct PyJulianDate {
pub(crate) inner: JulianDate<TT>,
}
impl PyJulianDate {
pub fn from_inner(inner: JulianDate<TT>) -> Self {
Self { inner }
}
}
#[allow(clippy::wrong_self_convention)]
#[pymethods]
impl PyJulianDate {
#[new]
fn new(value: f64) -> PyResult<Self> {
ensure_finite(value)?;
Ok(Self {
inner: JulianDate::<TT>::new(value),
})
}
#[staticmethod]
fn j2000() -> Self {
Self {
inner: JulianDate::<TT>::JD_EPOCH_J2000_0,
}
}
#[getter]
fn value(&self) -> f64 {
self.inner.value()
}
fn to_mjd(&self) -> PyModifiedJulianDate {
PyModifiedJulianDate::from_inner(self.inner.to::<MJD>())
}
fn to_utc(&self) -> PyResult<String> {
let dt = self
.inner
.to::<UTC>()
.try_to_chrono()
.map_err(map_conversion_error)?;
Ok(dt.to_rfc3339())
}
fn to_datetime<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
interop::time_to_datetime(py, self.inner.to::<UTC>().to_j2000s())
}
#[staticmethod]
fn from_utc(utc_str: &str) -> PyResult<Self> {
use chrono::{DateTime, FixedOffset, Utc};
let dt: DateTime<FixedOffset> = utc_str.parse().map_err(|e| {
pyo3::exceptions::PyValueError::new_err(format!("Invalid UTC datetime: {e}"))
})?;
let utc = tempoch::Time::<UTC>::try_from_chrono(dt.with_timezone(&Utc))
.map_err(map_conversion_error)?;
Ok(Self {
inner: utc.to::<TT>().to::<tempoch::JD>(),
})
}
#[staticmethod]
fn from_datetime(dt: &Bound<'_, PyAny>) -> PyResult<Self> {
let utc = interop::datetime_to_time(dt)?;
Ok(Self {
inner: utc.to::<TT>().to::<tempoch::JD>(),
})
}
fn julian_centuries(&self) -> f64 {
self.inner.julian_centuries()
}
fn julian_years(&self) -> f64 {
(self.inner - JulianDate::<TT>::JD_EPOCH_J2000_0)
.to::<JulianYear>()
.value()
}
fn julian_millennia(&self) -> f64 {
self.inner.julian_centuries() / 10.0
}
fn add_days(&self, days: f64) -> Self {
Self {
inner: self.inner + Day::new(days),
}
}
fn difference(&self, other: &PyJulianDate) -> f64 {
(self.inner - other.inner).value()
}
fn __add__(&self, days: f64) -> Self {
self.add_days(days)
}
fn __radd__(&self, days: f64) -> Self {
self.add_days(days)
}
fn __sub__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult<Py<PyAny>> {
let py = other.py();
if let Ok(other_jd) = other.extract::<PyJulianDate>() {
Ok(self
.difference(&other_jd)
.into_pyobject(py)?
.into_any()
.unbind())
} else if let Ok(days) = other.extract::<f64>() {
Ok(self.add_days(-days).into_pyobject(py)?.into_any().unbind())
} else {
Err(pyo3::exceptions::PyTypeError::new_err(
"Can only subtract a float (days) or another JulianDate",
))
}
}
fn __eq__(&self, other: &PyJulianDate) -> bool {
self.inner == other.inner
}
fn __ne__(&self, other: &PyJulianDate) -> bool {
self.inner != other.inner
}
fn __lt__(&self, other: &PyJulianDate) -> bool {
self.inner < other.inner
}
fn __le__(&self, other: &PyJulianDate) -> bool {
self.inner <= other.inner
}
fn __gt__(&self, other: &PyJulianDate) -> bool {
self.inner > other.inner
}
fn __ge__(&self, other: &PyJulianDate) -> bool {
self.inner >= other.inner
}
fn __repr__(&self) -> String {
format!("JulianDate({})", self.inner.value())
}
fn __str__(&self) -> String {
format!("JulianDate({})", self.inner.value())
}
fn __hash__(&self) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self.inner.value().to_bits().hash(&mut hasher);
hasher.finish()
}
fn __reduce__(&self, py: Python<'_>) -> PyResult<(Py<PyAny>, (f64,))> {
let cls = py.get_type::<Self>().into_any().unbind();
Ok((cls, (self.inner.value(),)))
}
fn __float__(&self) -> f64 {
self.inner.value()
}
}