use std::ops::{Add, Sub};
use std::str::FromStr;
use lox_test_utils::{ApproxEq, approx_eq};
use lox_time::subsecond::Subsecond;
use pyo3::basic::CompareOp;
use pyo3::exceptions::{PyException, PyTypeError, PyValueError};
use pyo3::types::{PyAnyMethods, PyType};
use pyo3::{
Bound, IntoPyObjectExt, Py, PyAny, PyErr, PyResult, Python, create_exception, pyclass,
pymethods,
};
use crate::earth::python::ut1::{PyEopProvider, PyEopProviderError};
use crate::time::calendar_dates::{CalendarDate, Date};
use crate::time::deltas::{TimeDelta, ToDelta};
use crate::time::julian_dates::{Epoch, JulianDate, Unit};
use crate::time::python::deltas::PyTimeDelta;
use crate::time::time_of_day::{CivilTime, TimeOfDay};
use crate::time::time_scales::Tai;
use crate::time::utc::transformations::ToUtc;
use crate::time::{DynTime, Time, TimeError, TimeScaleMismatch};
use super::time_scales::PyTimeScale;
use super::utc::PyUtc;
create_exception!(
lox_space,
NonFiniteTimeError,
PyException,
"Python exception raised when a non-finite `Time` is accessed."
);
pub struct PyTimeError(pub TimeError);
impl From<PyTimeError> for PyErr {
fn from(err: PyTimeError) -> Self {
PyValueError::new_err(err.0.to_string())
}
}
struct PyTimeScaleMismatch(TimeScaleMismatch);
impl From<PyTimeScaleMismatch> for PyErr {
fn from(err: PyTimeScaleMismatch) -> Self {
PyValueError::new_err(err.0.to_string())
}
}
pub struct PyEpoch(pub Epoch);
impl FromStr for PyEpoch {
type Err = PyErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"jd" | "JD" => Ok(Epoch::JulianDate),
"mjd" | "MJD" => Ok(Epoch::ModifiedJulianDate),
"j1950" | "J1950" => Ok(Epoch::J1950),
"j2000" | "J2000" => Ok(Epoch::J2000),
_ => Err(PyValueError::new_err(format!("unknown epoch: {s}"))),
}
.map(PyEpoch)
}
}
pub struct PyUnit(pub Unit);
impl FromStr for PyUnit {
type Err = PyErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"seconds" => Ok(Unit::Seconds),
"days" => Ok(Unit::Days),
"centuries" => Ok(Unit::Centuries),
_ => Err(PyValueError::new_err(format!("unknown unit: {s}"))),
}
.map(PyUnit)
}
}
#[pyclass(name = "Time", module = "lox_space", frozen, from_py_object)]
#[derive(Clone, Debug, Eq, PartialEq, ApproxEq)]
pub struct PyTime(pub DynTime);
#[pymethods]
impl PyTime {
#[new]
#[pyo3(signature=(scale, year, month, day, hour = 0, minute = 0, seconds = 0.0))]
pub fn new(
scale: &Bound<'_, PyAny>,
year: i64,
month: u8,
day: u8,
hour: u8,
minute: u8,
seconds: f64,
) -> PyResult<PyTime> {
let scale: PyTimeScale = scale.try_into()?;
let time = Time::builder_with_scale(scale.0)
.with_ymd(year, month, day)
.with_hms(hour, minute, seconds)
.build()
.map_err(PyTimeError)?;
Ok(PyTime(time))
}
fn __getnewargs__<'py>(
&self,
py: Python<'py>,
) -> (Bound<'py, PyAny>, i64, u8, u8, u8, u8, f64) {
(
self.scale().into_bound_py_any(py).unwrap(),
self.0.year(),
self.0.month(),
self.0.day(),
self.0.hour(),
self.0.minute(),
self.0.as_seconds_f64(),
)
}
#[classmethod]
#[pyo3(signature = (scale, jd, epoch = "jd"))]
pub fn from_julian_date(
_cls: &Bound<'_, PyType>,
scale: &Bound<'_, PyAny>,
jd: f64,
epoch: &str,
) -> PyResult<Self> {
let scale: PyTimeScale = scale.try_into()?;
let epoch: PyEpoch = epoch.parse()?;
Ok(Self(Time::from_julian_date(scale.0, jd, epoch.0)))
}
#[classmethod]
pub fn from_two_part_julian_date(
_cls: &Bound<'_, PyType>,
scale: &Bound<'_, PyAny>,
jd1: f64,
jd2: f64,
) -> PyResult<Self> {
let scale: PyTimeScale = scale.try_into()?;
Ok(Self(Time::from_two_part_julian_date(scale.0, jd1, jd2)))
}
#[classmethod]
#[pyo3(signature=(scale, year, day, hour=0, minute=0, seconds=0.0))]
pub fn from_day_of_year(
_cls: &Bound<'_, PyType>,
scale: &Bound<'_, PyAny>,
year: i64,
day: u16,
hour: u8,
minute: u8,
seconds: f64,
) -> PyResult<PyTime> {
let scale: PyTimeScale = scale.try_into()?;
let time = Time::builder_with_scale(scale.0)
.with_doy(year, day)
.with_hms(hour, minute, seconds)
.build()
.map_err(PyTimeError)?;
Ok(PyTime(time))
}
#[classmethod]
#[pyo3(signature = (iso, scale=None))]
pub fn from_iso(
_cls: &Bound<'_, PyType>,
iso: &str,
scale: Option<&Bound<'_, PyAny>>,
) -> PyResult<PyTime> {
let scale: PyTimeScale =
scale.map_or(Ok(PyTimeScale::default()), |scale| scale.try_into())?;
let time = Time::from_iso(scale.0, iso).map_err(PyTimeError)?;
Ok(PyTime(time))
}
#[classmethod]
pub fn from_seconds(
_cls: &Bound<'_, PyType>,
scale: &Bound<'_, PyAny>,
seconds: i64,
subsecond: f64,
) -> PyResult<PyTime> {
let scale: PyTimeScale = scale.try_into()?;
let subsecond =
Subsecond::from_f64(subsecond).ok_or(PyValueError::new_err("invalid subsecond"))?;
let time = Time::new(scale.0, seconds, subsecond);
Ok(PyTime(time))
}
pub fn seconds(&self) -> PyResult<i64> {
self.0.seconds().ok_or(NonFiniteTimeError::new_err(
"cannot access seconds for non-finite time",
))
}
pub fn subsecond(&self) -> PyResult<f64> {
self.0.subsecond().ok_or(NonFiniteTimeError::new_err(
"cannot access subsecond for non-finite time",
))
}
#[classattr]
const __hash__: Option<Py<PyAny>> = None;
pub fn __str__(&self) -> String {
self.0.to_string()
}
pub fn __repr__(&self) -> String {
format!(
"Time(\"{}\", {}, {}, {}, {}, {}, {})",
self.scale().abbreviation(),
self.0.year(),
self.0.month(),
self.0.day(),
self.0.hour(),
self.0.minute(),
self.0.as_seconds_f64(),
)
}
pub fn __add__(&self, delta: PyTimeDelta) -> Self {
PyTime(self.0 + delta.0)
}
pub fn __sub__<'py>(
&self,
py: Python<'py>,
rhs: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyAny>> {
if let Ok(delta) = rhs.extract::<PyTimeDelta>() {
Ok(Bound::new(py, PyTime(self.0 - delta.0))?.into_any())
} else if let Ok(rhs) = rhs.extract::<PyTime>() {
if self.scale() != rhs.scale() {
return Err(PyValueError::new_err(
"cannot subtract `Time` objects with different time scales",
));
}
Ok(Bound::new(py, PyTimeDelta(self.0 - rhs.0))?.into_any())
} else {
Err(PyTypeError::new_err(
"`rhs` must be either a `Time` or a `TimeDelta` object",
))
}
}
fn __richcmp__(&self, other: PyTime, op: CompareOp) -> PyResult<bool> {
let ordering = self.0.checked_cmp(&other.0).map_err(PyTimeScaleMismatch)?;
Ok(op.matches(ordering))
}
#[pyo3(signature = (rhs, rel_tol = 1e-8, abs_tol = 1e-14))]
pub fn isclose(&self, rhs: PyTime, rel_tol: f64, abs_tol: f64) -> PyResult<bool> {
if self.scale() != rhs.scale() {
return Err(PyValueError::new_err(
"cannot compare `Time` objects with different time scales",
));
}
Ok(approx_eq!(self, &rhs, rtol <= rel_tol, atol <= abs_tol))
}
#[pyo3(signature = (epoch = "jd", unit = "days"))]
pub fn julian_date(&self, epoch: &str, unit: &str) -> PyResult<f64> {
let epoch: PyEpoch = epoch.parse()?;
let unit: PyUnit = unit.parse()?;
Ok(self.0.julian_date(epoch.0, unit.0))
}
pub fn two_part_julian_date(&self) -> (f64, f64) {
self.0.two_part_julian_date()
}
pub fn scale(&self) -> PyTimeScale {
PyTimeScale(self.0.scale())
}
pub fn year(&self) -> i64 {
self.0.year()
}
pub fn month(&self) -> u8 {
self.0.month()
}
pub fn day(&self) -> u8 {
self.0.day()
}
pub fn day_of_year(&self) -> u16 {
self.0.day_of_year()
}
pub fn hour(&self) -> u8 {
self.0.hour()
}
pub fn minute(&self) -> u8 {
self.0.minute()
}
pub fn second(&self) -> u8 {
self.0.second()
}
pub fn millisecond(&self) -> u32 {
self.0.millisecond()
}
pub fn microsecond(&self) -> u32 {
self.0.microsecond()
}
pub fn nanosecond(&self) -> u32 {
self.0.nanosecond()
}
pub fn picosecond(&self) -> u32 {
self.0.picosecond()
}
pub fn femtosecond(&self) -> u32 {
self.0.femtosecond()
}
pub fn decimal_seconds(&self) -> f64 {
self.0.as_seconds_f64()
}
#[pyo3(signature = (scale, provider=None))]
pub fn to_scale(
&self,
scale: &Bound<'_, PyAny>,
provider: Option<&Bound<'_, PyEopProvider>>,
) -> PyResult<PyTime> {
let scale: PyTimeScale = scale.try_into()?;
let provider = provider.map(|p| &p.get().0);
let time = match provider {
Some(provider) => self
.0
.try_to_scale(scale.0, provider)
.map_err(PyEopProviderError)?,
None => self.0.to_scale(scale.0),
};
Ok(PyTime(time))
}
#[pyo3(signature = (provider=None))]
pub fn to_utc(&self, provider: Option<&Bound<'_, PyEopProvider>>) -> PyResult<PyUtc> {
let provider = provider.map(|p| &p.get().0);
let utc = match provider {
Some(provider) => self
.0
.try_to_scale(Tai, provider)
.map_err(PyEopProviderError)?,
None => self.0.to_scale(Tai),
}
.to_utc();
Ok(PyUtc(utc))
}
}
impl ToDelta for PyTime {
fn to_delta(&self) -> TimeDelta {
self.0.to_delta()
}
}
impl JulianDate for PyTime {
fn julian_date(&self, epoch: Epoch, unit: Unit) -> f64 {
self.0.julian_date(epoch, unit)
}
}
impl Add<TimeDelta> for PyTime {
type Output = PyTime;
fn add(self, rhs: TimeDelta) -> Self::Output {
PyTime(self.0 + rhs)
}
}
impl Sub<TimeDelta> for PyTime {
type Output = PyTime;
fn sub(self, rhs: TimeDelta) -> Self::Output {
PyTime(self.0 - rhs)
}
}
impl Sub<PyTime> for PyTime {
type Output = TimeDelta;
fn sub(self, rhs: PyTime) -> TimeDelta {
self.0 - rhs.0
}
}
impl CalendarDate for PyTime {
fn date(&self) -> Date {
self.0.date()
}
}
impl CivilTime for PyTime {
fn time(&self) -> TimeOfDay {
self.0.time()
}
}