use core::fmt;
use core::marker::PhantomData;
use crate::context::TimeContext;
use crate::encoding::{
j2000_seconds_to_jd, j2000_seconds_to_mjd, jd_to_j2000_seconds, mjd_to_j2000_seconds,
};
use crate::error::ConversionError;
use crate::scale::conversion::InfallibleScaleConvert;
use crate::scale::{CoordinateScale, Scale, TAI, UTC};
use crate::sealed::Sealed;
use crate::target::{ContextConversionTarget, ConversionTarget, InfallibleConversionTarget};
use crate::time::Time;
use qtty::{Day, Quantity, Second, Unit};
#[allow(private_bounds)]
pub trait TimeFormat: Sealed + Copy + Clone + fmt::Debug + 'static {
type Unit: Unit;
const NAME: &'static str;
}
#[allow(private_bounds)]
pub trait FormatForScale<S: Scale>: TimeFormat + Sealed {
fn try_from_time(
time: Time<S>,
ctx: &TimeContext,
) -> Result<Quantity<Self::Unit>, ConversionError>;
fn try_into_time(
raw: Quantity<Self::Unit>,
ctx: &TimeContext,
) -> Result<Time<S>, ConversionError>;
}
#[allow(private_bounds)]
pub trait InfallibleFormatForScale<S: Scale>: FormatForScale<S> + Sealed {
fn from_time(time: Time<S>) -> Quantity<Self::Unit>;
fn into_time(raw: Quantity<Self::Unit>) -> Time<S>;
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct J2000s;
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct JD;
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MJD;
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Unix;
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GPS;
impl Sealed for J2000s {}
impl Sealed for JD {}
impl Sealed for MJD {}
impl Sealed for Unix {}
impl Sealed for GPS {}
impl TimeFormat for J2000s {
type Unit = qtty::unit::Second;
const NAME: &'static str = "J2000s";
}
impl TimeFormat for JD {
type Unit = qtty::unit::Day;
const NAME: &'static str = "JD";
}
impl TimeFormat for MJD {
type Unit = qtty::unit::Day;
const NAME: &'static str = "MJD";
}
impl TimeFormat for Unix {
type Unit = qtty::unit::Second;
const NAME: &'static str = "Unix";
}
impl TimeFormat for GPS {
type Unit = qtty::unit::Second;
const NAME: &'static str = "GPS";
}
pub struct EncodedTime<S: Scale, F: TimeFormat> {
raw: Quantity<F::Unit>,
_marker: PhantomData<fn() -> S>,
}
impl<S: Scale, F: TimeFormat> Copy for EncodedTime<S, F> {}
impl<S: Scale, F: TimeFormat> Clone for EncodedTime<S, F> {
#[inline]
fn clone(&self) -> Self {
*self
}
}
impl<S: Scale, F: TimeFormat> fmt::Debug for EncodedTime<S, F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EncodedTime")
.field("scale", &S::NAME)
.field("format", &F::NAME)
.field("raw", &self.raw)
.finish()
}
}
impl<S: Scale, F: TimeFormat> fmt::Display for EncodedTime<S, F>
where
qtty::Quantity<F::Unit>: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.raw, f)
}
}
impl<S: Scale, F: TimeFormat> fmt::LowerExp for EncodedTime<S, F>
where
qtty::Quantity<F::Unit>: fmt::LowerExp,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::LowerExp::fmt(&self.raw, f)
}
}
impl<S: Scale, F: TimeFormat> fmt::UpperExp for EncodedTime<S, F>
where
qtty::Quantity<F::Unit>: fmt::UpperExp,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::UpperExp::fmt(&self.raw, f)
}
}
impl<S: Scale, F: TimeFormat> PartialEq for EncodedTime<S, F> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.raw == other.raw
}
}
impl<S: Scale, F: TimeFormat> PartialOrd for EncodedTime<S, F> {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
self.raw.partial_cmp(&other.raw)
}
}
impl<S: Scale, F: TimeFormat> EncodedTime<S, F> {
#[inline]
pub(crate) const fn new_unchecked(raw: Quantity<F::Unit>) -> Self {
Self {
raw,
_marker: PhantomData,
}
}
#[inline]
pub const fn from_raw_unchecked(raw: Quantity<F::Unit>) -> Self {
Self::new_unchecked(raw)
}
#[inline]
pub const fn raw(self) -> Quantity<F::Unit> {
self.raw
}
#[inline]
pub const fn quantity(self) -> Quantity<F::Unit> {
self.raw
}
}
impl<S: Scale> EncodedTime<S, JD> {
pub const J2000: Self = Self::from_raw_unchecked(crate::constats::J2000_JD_TT.raw());
}
impl<S: Scale, F> EncodedTime<S, F>
where
F: FormatForScale<S>,
{
#[inline]
pub fn try_new(raw: Quantity<F::Unit>) -> Result<Self, ConversionError> {
if raw.is_finite() {
Ok(Self::new_unchecked(raw))
} else {
Err(ConversionError::NonFinite)
}
}
#[inline]
pub fn try_to_time(self) -> Result<Time<S>, ConversionError> {
F::try_into_time(self.raw, &TimeContext::new())
}
#[inline]
pub fn to_time_with(self, ctx: &TimeContext) -> Result<Time<S>, ConversionError> {
F::try_into_time(self.raw, ctx)
}
}
impl<S: Scale, F> EncodedTime<S, F>
where
F: InfallibleFormatForScale<S>,
{
#[inline]
pub(crate) fn from_time_infallible(time: Time<S>) -> Self {
Self::new_unchecked(F::from_time(time))
}
#[inline]
pub fn to_time(self) -> Time<S> {
F::into_time(self.raw)
}
#[allow(private_bounds)]
#[inline]
pub fn to<T>(self) -> T::Output
where
T: InfallibleConversionTarget<S>,
{
T::convert(self.to_time())
}
#[allow(private_bounds)]
#[inline]
pub fn try_to<T>(self) -> Result<T::Output, ConversionError>
where
T: ConversionTarget<S>,
{
T::try_convert(self.to_time())
}
}
impl<S: Scale, F> EncodedTime<S, F>
where
F: FormatForScale<S>,
{
#[allow(private_bounds)]
#[inline]
pub fn to_with<T>(self, ctx: &TimeContext) -> Result<T::Output, ConversionError>
where
T: ContextConversionTarget<S>,
{
T::convert_with(self.to_time_with(ctx)?, ctx)
}
}
pub type JulianDate<S> = EncodedTime<S, JD>;
pub type ModifiedJulianDate<S> = EncodedTime<S, MJD>;
pub type J2000Seconds<S> = EncodedTime<S, J2000s>;
pub type UnixTime = EncodedTime<UTC, Unix>;
pub type GpsTime = EncodedTime<TAI, GPS>;
macro_rules! coordinate_format {
($fmt:ty, $quantity:ty, $from_time:expr, $to_time:expr) => {
impl<S: CoordinateScale> FormatForScale<S> for $fmt {
#[inline]
fn try_from_time(
time: Time<S>,
_ctx: &TimeContext,
) -> Result<$quantity, ConversionError> {
Ok(<Self as InfallibleFormatForScale<S>>::from_time(time))
}
#[inline]
fn try_into_time(
raw: $quantity,
_ctx: &TimeContext,
) -> Result<Time<S>, ConversionError> {
Ok(<Self as InfallibleFormatForScale<S>>::into_time(raw))
}
}
impl<S: CoordinateScale> InfallibleFormatForScale<S> for $fmt {
#[inline]
fn from_time(time: Time<S>) -> $quantity {
$from_time(time)
}
#[inline]
fn into_time(raw: $quantity) -> Time<S> {
$to_time(raw)
}
}
};
}
coordinate_format!(
J2000s,
Second,
|time: Time<_>| time.raw_j2000_seconds(),
|raw: Second| Time::from_raw_j2000_seconds(raw).expect("finite J2000 seconds must decode")
);
coordinate_format!(
JD,
Day,
|time: Time<_>| j2000_seconds_to_jd(time.raw_j2000_seconds()),
|raw: Day| Time::from_raw_j2000_seconds(jd_to_j2000_seconds(raw))
.expect("finite Julian date must decode")
);
coordinate_format!(
MJD,
Day,
|time: Time<_>| j2000_seconds_to_mjd(time.raw_j2000_seconds()),
|raw: Day| Time::from_raw_j2000_seconds(mjd_to_j2000_seconds(raw))
.expect("finite Modified Julian date must decode")
);
impl FormatForScale<UTC> for Unix {
#[inline]
fn try_from_time(time: Time<UTC>, ctx: &TimeContext) -> Result<Second, ConversionError> {
time.raw_unix_seconds_with(ctx)
}
#[inline]
fn try_into_time(raw: Second, ctx: &TimeContext) -> Result<Time<UTC>, ConversionError> {
Time::from_raw_unix_seconds_with(raw, ctx)
}
}
impl FormatForScale<TAI> for GPS {
#[inline]
fn try_from_time(time: Time<TAI>, _ctx: &TimeContext) -> Result<Second, ConversionError> {
Ok(<Self as InfallibleFormatForScale<TAI>>::from_time(time))
}
#[inline]
fn try_into_time(raw: Second, _ctx: &TimeContext) -> Result<Time<TAI>, ConversionError> {
Ok(<Self as InfallibleFormatForScale<TAI>>::into_time(raw))
}
}
impl InfallibleFormatForScale<TAI> for GPS {
#[inline]
fn from_time(time: Time<TAI>) -> Second {
time.raw_gps_seconds()
}
#[inline]
fn into_time(raw: Second) -> Time<TAI> {
Time::from_raw_gps_seconds(raw).expect("finite GPS seconds must decode")
}
}
impl<S: Scale, F> From<EncodedTime<S, F>> for Time<S>
where
F: InfallibleFormatForScale<S>,
{
#[inline]
fn from(value: EncodedTime<S, F>) -> Self {
value.to_time()
}
}
impl<S: Scale, F> From<Time<S>> for EncodedTime<S, F>
where
F: InfallibleFormatForScale<S>,
{
#[inline]
fn from(value: Time<S>) -> Self {
Self::from_time_infallible(value)
}
}
impl<S: CoordinateScale> ConversionTarget<S> for J2000s {
type Output = EncodedTime<S, J2000s>;
#[inline]
fn try_convert(src: Time<S>) -> Result<Self::Output, ConversionError> {
Ok(EncodedTime::from_time_infallible(src))
}
}
impl<S: CoordinateScale> InfallibleConversionTarget<S> for J2000s {
#[inline]
fn convert(src: Time<S>) -> Self::Output {
EncodedTime::from_time_infallible(src)
}
}
impl<S: CoordinateScale> ConversionTarget<S> for JD {
type Output = EncodedTime<S, JD>;
#[inline]
fn try_convert(src: Time<S>) -> Result<Self::Output, ConversionError> {
Ok(EncodedTime::from_time_infallible(src))
}
}
impl<S: CoordinateScale> InfallibleConversionTarget<S> for JD {
#[inline]
fn convert(src: Time<S>) -> Self::Output {
EncodedTime::from_time_infallible(src)
}
}
impl<S: CoordinateScale> ConversionTarget<S> for MJD {
type Output = EncodedTime<S, MJD>;
#[inline]
fn try_convert(src: Time<S>) -> Result<Self::Output, ConversionError> {
Ok(EncodedTime::from_time_infallible(src))
}
}
impl<S: CoordinateScale> InfallibleConversionTarget<S> for MJD {
#[inline]
fn convert(src: Time<S>) -> Self::Output {
EncodedTime::from_time_infallible(src)
}
}
impl<S> ConversionTarget<S> for Unix
where
S: crate::scale::Scale + InfallibleScaleConvert<UTC>,
{
type Output = EncodedTime<UTC, Unix>;
#[inline]
fn try_convert(src: Time<S>) -> Result<Self::Output, ConversionError> {
let utc = src.to_scale::<UTC>();
let raw = Unix::try_from_time(utc, &TimeContext::new())?;
Ok(EncodedTime::new_unchecked(raw))
}
}
impl ContextConversionTarget<UTC> for Unix {
type Output = EncodedTime<UTC, Unix>;
#[inline]
fn convert_with(src: Time<UTC>, ctx: &TimeContext) -> Result<Self::Output, ConversionError> {
let raw = Unix::try_from_time(src, ctx)?;
Ok(EncodedTime::new_unchecked(raw))
}
}
impl<S> ContextConversionTarget<S> for Unix
where
S: crate::scale::Scale + crate::scale::conversion::ContextScaleConvert<UTC>,
{
type Output = EncodedTime<UTC, Unix>;
#[inline]
fn convert_with(src: Time<S>, ctx: &TimeContext) -> Result<Self::Output, ConversionError> {
let utc = src.to_scale_with::<UTC>(ctx)?;
let raw = Unix::try_from_time(utc, ctx)?;
Ok(EncodedTime::new_unchecked(raw))
}
}
impl<S> ConversionTarget<S> for GPS
where
S: crate::scale::Scale + InfallibleScaleConvert<TAI>,
{
type Output = EncodedTime<TAI, GPS>;
#[inline]
fn try_convert(src: Time<S>) -> Result<Self::Output, ConversionError> {
Ok(Self::convert(src))
}
}
impl<S> InfallibleConversionTarget<S> for GPS
where
S: crate::scale::Scale + InfallibleScaleConvert<TAI>,
{
#[inline]
fn convert(src: Time<S>) -> Self::Output {
EncodedTime::from_time_infallible(src.to_scale::<TAI>())
}
}
impl<S> ContextConversionTarget<S> for GPS
where
S: crate::scale::Scale + crate::scale::conversion::ContextScaleConvert<TAI>,
{
type Output = EncodedTime<TAI, GPS>;
#[inline]
fn convert_with(src: Time<S>, ctx: &TimeContext) -> Result<Self::Output, ConversionError> {
let tai = src.to_scale_with::<TAI>(ctx)?;
Ok(EncodedTime::from_time_infallible(tai))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::TimeContext;
use crate::scale::{TAI, TT, UTC};
use qtty::{Day, Second};
#[test]
fn encoded_time_display_delegates_to_quantity() {
let jd = JulianDate::<TT>::try_new(Day::new(2_451_545.123_456_789)).unwrap();
assert_eq!(format!("{jd:.9}"), "2451545.123456789 d");
}
#[test]
fn encoded_time_lower_exp_delegates_to_quantity() {
let seconds = J2000Seconds::<TT>::try_new(Second::new(1_234.5)).unwrap();
let formatted = format!("{seconds:.2e}");
assert_eq!(formatted, format!("{:.2e}", seconds.raw()));
}
#[test]
fn encoded_time_upper_exp_delegates_to_quantity() {
let seconds = J2000Seconds::<TT>::try_new(Second::new(1_234.5)).unwrap();
let formatted = format!("{seconds:.2E}");
assert_eq!(formatted, format!("{:.2E}", seconds.raw()));
}
#[test]
fn encoded_time_clone_matches_original() {
let jd = JulianDate::<TT>::try_new(Day::new(2_451_545.0)).unwrap();
let cloned = <JulianDate<TT> as Clone>::clone(&jd);
assert_eq!(jd.raw(), cloned.raw());
}
#[test]
fn encoded_time_partial_eq() {
let a = JulianDate::<TT>::try_new(Day::new(2_451_545.0)).unwrap();
let b = JulianDate::<TT>::try_new(Day::new(2_451_545.0)).unwrap();
let c = JulianDate::<TT>::try_new(Day::new(2_451_546.0)).unwrap();
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn encoded_time_quantity_is_alias_for_raw() {
let jd = JulianDate::<TT>::try_new(Day::new(2_451_545.5)).unwrap();
assert_eq!(jd.raw(), jd.quantity());
}
#[test]
fn encoded_time_try_to_time_on_unix() {
let ctx = TimeContext::new();
let unix = UnixTime::try_new(Second::new(946_727_935.816)).unwrap();
let time = unix.to_time_with(&ctx).unwrap();
let back = <Unix as FormatForScale<UTC>>::try_from_time(time, &ctx).unwrap();
assert!((back - Second::new(946_727_935.816)).abs() < Second::new(1e-3));
}
#[test]
fn encoded_time_to_infallible_conversion() {
let jd = JulianDate::<TT>::try_new(Day::new(2_451_545.0)).unwrap();
let mjd: ModifiedJulianDate<TT> = jd.to::<MJD>();
assert!((mjd.raw().value() - 51_544.5).abs() < 1e-9);
}
#[test]
fn encoded_time_try_to_conversion() {
let jd = JulianDate::<TT>::try_new(Day::new(2_451_545.0)).unwrap();
let mjd: ModifiedJulianDate<TT> = jd.try_to::<MJD>().unwrap();
assert!((mjd.raw().value() - 51_544.5).abs() < 1e-9);
}
#[test]
fn encoded_time_to_with_for_unix() {
let ctx = TimeContext::new();
let jd = JulianDate::<UTC>::try_new(Day::new(2_451_545.0)).unwrap();
let unix: UnixTime = jd.to_with::<Unix>(&ctx).unwrap();
assert!(unix.raw().value().is_finite());
assert!(unix.raw().value() > 9e8 && unix.raw().value() < 1e10);
}
#[test]
fn gps_format_roundtrip_through_tai() {
let gps_seconds = Second::new(0.0);
let time: crate::time::Time<TAI> =
<GPS as InfallibleFormatForScale<TAI>>::into_time(gps_seconds);
let back = <GPS as InfallibleFormatForScale<TAI>>::from_time(time);
assert!((back - gps_seconds).abs() < Second::new(1e-12));
}
#[test]
fn gps_encoded_time_to_time_roundtrip() {
let gps = GpsTime::try_new(Second::new(1_234_567.89)).unwrap();
let time = gps.to_time();
let back: GpsTime = time.into();
assert!((back.raw() - gps.raw()).abs() < Second::new(1e-6));
}
#[test]
fn from_encoded_time_into_time() {
let jd = JulianDate::<TT>::try_new(Day::new(2_451_545.0)).unwrap();
let time: crate::time::Time<TT> = jd.into();
let back: JulianDate<TT> = time.into();
assert!((back.raw() - Day::new(2_451_545.0)).abs() < Day::new(1e-12));
}
#[test]
fn infallible_conversion_target_for_j2000s() {
let jd = JulianDate::<TT>::try_new(Day::new(2_451_545.0)).unwrap();
let time = jd.to_time();
let j2k: J2000Seconds<TT> = J2000s::convert(time);
assert!((j2k.raw().value()).abs() < 1e-6);
}
#[test]
fn conversion_target_try_convert_for_j2000s() {
let jd = JulianDate::<TT>::try_new(Day::new(2_451_545.0)).unwrap();
let time = jd.to_time();
let j2k: J2000Seconds<TT> = J2000s::try_convert(time).unwrap();
assert!((j2k.raw().value()).abs() < 1e-6);
}
#[test]
fn conversion_target_try_convert_for_jd() {
let mjd = ModifiedJulianDate::<TT>::try_new(Day::new(51_544.0)).unwrap();
let time = mjd.to_time();
let jd: JulianDate<TT> = JD::try_convert(time).unwrap();
assert!((jd.raw().value() - 2_451_544.5).abs() < 1e-9);
}
#[test]
fn conversion_target_try_convert_for_mjd() {
let jd = JulianDate::<TT>::try_new(Day::new(2_451_545.0)).unwrap();
let time = jd.to_time();
let mjd: ModifiedJulianDate<TT> = MJD::try_convert(time).unwrap();
assert!((mjd.raw().value() - 51_544.5).abs() < 1e-9);
}
#[test]
fn gps_conversion_target_try_convert() {
let jd = JulianDate::<TT>::try_new(Day::new(2_451_545.0)).unwrap();
let time = jd.to_time();
let gps: GpsTime = GPS::try_convert(time).unwrap();
assert!(gps.raw().is_finite());
}
#[test]
fn unix_context_conversion_target() {
let ctx = TimeContext::new();
let jd = JulianDate::<UTC>::try_new(Day::new(2_451_545.0)).unwrap();
let utc_time = jd.to_time();
let unix =
<Unix as crate::target::ContextConversionTarget<UTC>>::convert_with(utc_time, &ctx)
.unwrap();
assert!(unix.raw().value().is_finite());
assert!(unix.raw().value() > 9e8 && unix.raw().value() < 1e10);
}
#[test]
fn debug_includes_format_and_scale() {
let jd = JulianDate::<TT>::try_new(Day::new(2_451_545.0)).unwrap();
let dbg = format!("{jd:?}");
assert!(dbg.contains("TT"), "debug should contain scale name");
assert!(dbg.contains("JD"), "debug should contain format name");
}
#[test]
fn jd_on_tt_and_utc_are_distinct_types() {
fn accept_tt(x: EncodedTime<TT, JD>) -> Day {
x.raw()
}
fn accept_utc(x: EncodedTime<UTC, JD>) -> Day {
x.raw()
}
let tt_jd = JulianDate::<TT>::try_new(Day::new(2_451_545.0)).unwrap();
let utc_jd = JulianDate::<UTC>::try_new(Day::new(2_451_545.0)).unwrap();
let _ = accept_tt(tt_jd);
let _ = accept_utc(utc_jd);
}
#[test]
fn format_names_are_correct() {
assert_eq!(JD::NAME, "JD");
assert_eq!(MJD::NAME, "MJD");
assert_eq!(J2000s::NAME, "J2000s");
assert_eq!(Unix::NAME, "Unix");
assert_eq!(GPS::NAME, "GPS");
}
}