use core::fmt;
use core::time::Duration;
use crate::angles::Angle;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
pub struct JulianDay(f64);
impl JulianDay {
pub const fn from_days(days: f64) -> Self {
Self(days)
}
pub const fn days(self) -> f64 {
self.0
}
pub fn add_seconds(self, seconds: f64) -> Self {
Self(self.0 + seconds / SECONDS_PER_DAY)
}
}
impl fmt::Display for JulianDay {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "JD {}", self.0)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum TimeScale {
Utc,
Ut1,
Tt,
Tdb,
}
impl fmt::Display for TimeScale {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::Utc => "UTC",
Self::Ut1 => "UT1",
Self::Tt => "TT",
Self::Tdb => "TDB",
};
f.write_str(label)
}
}
pub const SECONDS_PER_DAY: f64 = 86_400.0;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TimeScaleConversionError {
Expected {
expected: TimeScale,
actual: TimeScale,
},
NonFiniteOffset,
}
impl TimeScaleConversionError {
pub(crate) const fn expected(expected: TimeScale, actual: TimeScale) -> Self {
Self::Expected { expected, actual }
}
pub(crate) const fn non_finite_offset() -> Self {
Self::NonFiniteOffset
}
pub fn summary_line(&self) -> String {
match self {
Self::Expected { expected, actual } => format!(
"time-scale conversion expected {}, got {}",
expected, actual
),
Self::NonFiniteOffset => "time-scale conversion offset must be finite".to_string(),
}
}
}
impl fmt::Display for TimeScaleConversionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.summary_line())
}
}
impl std::error::Error for TimeScaleConversionError {}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TimeScaleConversion {
pub source: TimeScale,
pub target: TimeScale,
pub offset_seconds: f64,
}
impl TimeScaleConversion {
pub const fn new(source: TimeScale, target: TimeScale, offset_seconds: f64) -> Self {
Self {
source,
target,
offset_seconds,
}
}
pub fn summary_line(&self) -> String {
format!(
"source={}; target={}; offset_seconds={} s",
self.source, self.target, self.offset_seconds
)
}
pub fn validated_summary_line(
&self,
instant: Instant,
) -> Result<String, TimeScaleConversionError> {
self.validate(instant)?;
Ok(self.summary_line())
}
pub fn validate(self, instant: Instant) -> Result<(), TimeScaleConversionError> {
if instant.scale != self.source {
return Err(TimeScaleConversionError::expected(
self.source,
instant.scale,
));
}
checked_time_scale_offset(self.offset_seconds)?;
Ok(())
}
pub fn apply(self, instant: Instant) -> Result<Instant, TimeScaleConversionError> {
self.validate(instant)?;
Ok(instant.with_time_scale_offset(self.target, self.offset_seconds))
}
}
impl fmt::Display for TimeScaleConversion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.summary_line())
}
}
pub(crate) fn checked_time_scale_offset(
offset_seconds: f64,
) -> Result<f64, TimeScaleConversionError> {
if offset_seconds.is_finite() {
Ok(offset_seconds)
} else {
Err(TimeScaleConversionError::non_finite_offset())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Instant {
pub julian_day: JulianDay,
pub scale: TimeScale,
}
impl Instant {
pub const fn new(julian_day: JulianDay, scale: TimeScale) -> Self {
Self { julian_day, scale }
}
pub fn summary_line(&self) -> String {
format!("{} {}", self.julian_day, self.scale)
}
pub fn with_time_scale_conversion(
self,
conversion: TimeScaleConversion,
) -> Result<Self, TimeScaleConversionError> {
conversion.apply(self)
}
pub fn validate_time_scale_conversion(
self,
conversion: TimeScaleConversion,
) -> Result<(), TimeScaleConversionError> {
conversion.validate(self)
}
pub fn with_time_scale_offset(self, target_scale: TimeScale, offset_seconds: f64) -> Self {
Self {
julian_day: self.julian_day.add_seconds(offset_seconds),
scale: target_scale,
}
}
pub fn with_time_scale_offset_checked(
self,
target_scale: TimeScale,
offset_seconds: f64,
) -> Result<Self, TimeScaleConversionError> {
TimeScaleConversion::new(self.scale, target_scale, offset_seconds).apply(self)
}
pub fn mean_obliquity(self) -> Angle {
let t = (self.julian_day.days() - 2_451_545.0) / 36_525.0;
Angle::from_degrees(
23.439_291_111_111_11
- 0.013_004_166_666_666_667 * t
- 0.000_000_163_888_888_888_888_88 * t * t
+ 0.000_000_503_611_111_111_111_1 * t * t * t,
)
}
pub fn tt_from_ut1(self, delta_t: Duration) -> Result<Self, TimeScaleConversionError> {
if self.scale != TimeScale::Ut1 {
return Err(TimeScaleConversionError::expected(
TimeScale::Ut1,
self.scale,
));
}
Ok(self.with_time_scale_offset(TimeScale::Tt, delta_t.as_secs_f64()))
}
pub fn tt_from_ut1_signed(self, offset_seconds: f64) -> Result<Self, TimeScaleConversionError> {
if self.scale != TimeScale::Ut1 {
return Err(TimeScaleConversionError::expected(
TimeScale::Ut1,
self.scale,
));
}
let offset_seconds = checked_time_scale_offset(offset_seconds)?;
Ok(self.with_time_scale_offset(TimeScale::Tt, offset_seconds))
}
pub fn tt_from_utc(self, delta_t: Duration) -> Result<Self, TimeScaleConversionError> {
if self.scale != TimeScale::Utc {
return Err(TimeScaleConversionError::expected(
TimeScale::Utc,
self.scale,
));
}
Ok(self.with_time_scale_offset(TimeScale::Tt, delta_t.as_secs_f64()))
}
pub fn tt_from_utc_signed(self, offset_seconds: f64) -> Result<Self, TimeScaleConversionError> {
if self.scale != TimeScale::Utc {
return Err(TimeScaleConversionError::expected(
TimeScale::Utc,
self.scale,
));
}
let offset_seconds = checked_time_scale_offset(offset_seconds)?;
Ok(self.with_time_scale_offset(TimeScale::Tt, offset_seconds))
}
pub fn tdb_from_tt(self, offset: Duration) -> Result<Self, TimeScaleConversionError> {
if self.scale != TimeScale::Tt {
return Err(TimeScaleConversionError::expected(
TimeScale::Tt,
self.scale,
));
}
Ok(self.with_time_scale_offset(TimeScale::Tdb, offset.as_secs_f64()))
}
pub fn tdb_from_tt_signed(self, offset_seconds: f64) -> Result<Self, TimeScaleConversionError> {
if self.scale != TimeScale::Tt {
return Err(TimeScaleConversionError::expected(
TimeScale::Tt,
self.scale,
));
}
let offset_seconds = checked_time_scale_offset(offset_seconds)?;
Ok(self.with_time_scale_offset(TimeScale::Tdb, offset_seconds))
}
pub fn tt_from_tdb(self, offset_seconds: f64) -> Result<Self, TimeScaleConversionError> {
if self.scale != TimeScale::Tdb {
return Err(TimeScaleConversionError::expected(
TimeScale::Tdb,
self.scale,
));
}
let offset_seconds = checked_time_scale_offset(offset_seconds)?;
Ok(self.with_time_scale_offset(TimeScale::Tt, offset_seconds))
}
pub fn tt_from_tdb_signed(self, offset_seconds: f64) -> Result<Self, TimeScaleConversionError> {
self.tt_from_tdb(offset_seconds)
}
pub fn tdb_from_ut1(
self,
tt_offset: Duration,
tdb_offset: Duration,
) -> Result<Self, TimeScaleConversionError> {
let tt = self.tt_from_ut1(tt_offset)?;
tt.tdb_from_tt(tdb_offset)
}
pub fn tdb_from_ut1_signed(
self,
tt_offset: Duration,
tdb_offset_seconds: f64,
) -> Result<Self, TimeScaleConversionError> {
let tt = self.tt_from_ut1(tt_offset)?;
tt.tdb_from_tt_signed(tdb_offset_seconds)
}
pub fn tdb_from_utc(
self,
tt_offset: Duration,
tdb_offset: Duration,
) -> Result<Self, TimeScaleConversionError> {
let tt = self.tt_from_utc(tt_offset)?;
tt.tdb_from_tt(tdb_offset)
}
pub fn tdb_from_utc_signed(
self,
tt_offset: Duration,
tdb_offset_seconds: f64,
) -> Result<Self, TimeScaleConversionError> {
let tt = self.tt_from_utc(tt_offset)?;
tt.tdb_from_tt_signed(tdb_offset_seconds)
}
}
impl fmt::Display for Instant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.summary_line())
}
}