1use core::fmt;
7use core::marker::PhantomData;
8
9use crate::context::TimeContext;
10use crate::encoding::{
11 j2000_seconds_to_jd, j2000_seconds_to_mjd, jd_to_j2000_seconds, mjd_to_j2000_seconds,
12};
13use crate::error::ConversionError;
14use crate::scale::conversion::InfallibleScaleConvert;
15use crate::scale::{CoordinateScale, Scale, TAI, UTC};
16use crate::sealed::Sealed;
17use crate::target::{ContextConversionTarget, ConversionTarget, InfallibleConversionTarget};
18use crate::time::Time;
19use qtty::{Day, Quantity, Second, Unit};
20
21#[allow(private_bounds)]
23pub trait TimeRepresentation: Sealed + Copy + Clone + fmt::Debug + 'static {
24 type Unit: Unit;
26
27 const NAME: &'static str;
29}
30
31#[allow(private_bounds)]
33pub trait RepresentationForScale<S: Scale>: TimeRepresentation + Sealed {
34 fn try_from_time(
35 time: Time<S>,
36 ctx: &TimeContext,
37 ) -> Result<Quantity<Self::Unit>, ConversionError>;
38 fn try_into_time(
39 raw: Quantity<Self::Unit>,
40 ctx: &TimeContext,
41 ) -> Result<Time<S>, ConversionError>;
42}
43
44#[allow(private_bounds)]
46pub trait InfallibleRepresentationForScale<S: Scale>: RepresentationForScale<S> + Sealed {
47 fn from_time(time: Time<S>) -> Quantity<Self::Unit>;
48 fn into_time(raw: Quantity<Self::Unit>) -> Time<S>;
49}
50
51#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
53pub struct J2000s;
54
55#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
57pub struct JD;
58
59#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61pub struct MJD;
62
63#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
65pub struct Unix;
66
67#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
69pub struct GPS;
70
71impl Sealed for J2000s {}
72impl Sealed for JD {}
73impl Sealed for MJD {}
74impl Sealed for Unix {}
75impl Sealed for GPS {}
76
77impl TimeRepresentation for J2000s {
78 type Unit = qtty::unit::Second;
79 const NAME: &'static str = "J2000s";
80}
81
82impl TimeRepresentation for JD {
83 type Unit = qtty::unit::Day;
84 const NAME: &'static str = "JD";
85}
86
87impl TimeRepresentation for MJD {
88 type Unit = qtty::unit::Day;
89 const NAME: &'static str = "MJD";
90}
91
92impl TimeRepresentation for Unix {
93 type Unit = qtty::unit::Second;
94 const NAME: &'static str = "Unix";
95}
96
97impl TimeRepresentation for GPS {
98 type Unit = qtty::unit::Second;
99 const NAME: &'static str = "GPS";
100}
101
102pub struct EncodedTime<S: Scale, R: TimeRepresentation> {
104 raw: Quantity<R::Unit>,
105 _marker: PhantomData<fn() -> S>,
106}
107
108impl<S: Scale, R: TimeRepresentation> Copy for EncodedTime<S, R> {}
109
110impl<S: Scale, R: TimeRepresentation> Clone for EncodedTime<S, R> {
111 #[inline]
112 fn clone(&self) -> Self {
113 *self
114 }
115}
116
117impl<S: Scale, R: TimeRepresentation> fmt::Debug for EncodedTime<S, R> {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 f.debug_struct("EncodedTime")
120 .field("scale", &S::NAME)
121 .field("representation", &R::NAME)
122 .field("raw", &self.raw)
123 .finish()
124 }
125}
126
127impl<S: Scale, R: TimeRepresentation> fmt::Display for EncodedTime<S, R>
128where
129 qtty::Quantity<R::Unit>: fmt::Display,
130{
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 fmt::Display::fmt(&self.raw, f)
133 }
134}
135
136impl<S: Scale, R: TimeRepresentation> fmt::LowerExp for EncodedTime<S, R>
137where
138 qtty::Quantity<R::Unit>: fmt::LowerExp,
139{
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 fmt::LowerExp::fmt(&self.raw, f)
142 }
143}
144
145impl<S: Scale, R: TimeRepresentation> fmt::UpperExp for EncodedTime<S, R>
146where
147 qtty::Quantity<R::Unit>: fmt::UpperExp,
148{
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 fmt::UpperExp::fmt(&self.raw, f)
151 }
152}
153
154impl<S: Scale, R: TimeRepresentation> PartialEq for EncodedTime<S, R> {
155 #[inline]
156 fn eq(&self, other: &Self) -> bool {
157 self.raw == other.raw
158 }
159}
160
161impl<S: Scale, R: TimeRepresentation> PartialOrd for EncodedTime<S, R> {
162 #[inline]
163 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
164 self.raw.partial_cmp(&other.raw)
165 }
166}
167
168impl<S: Scale, R: TimeRepresentation> EncodedTime<S, R> {
169 #[inline]
170 pub(crate) const fn new_unchecked(raw: Quantity<R::Unit>) -> Self {
171 Self {
172 raw,
173 _marker: PhantomData,
174 }
175 }
176
177 #[inline]
179 pub const fn raw(self) -> Quantity<R::Unit> {
180 self.raw
181 }
182
183 #[inline]
185 pub const fn quantity(self) -> Quantity<R::Unit> {
186 self.raw
187 }
188}
189
190impl<S: Scale, R> EncodedTime<S, R>
191where
192 R: RepresentationForScale<S>,
193{
194 #[inline]
196 pub fn try_new(raw: Quantity<R::Unit>) -> Result<Self, ConversionError> {
197 if raw.is_finite() {
198 Ok(Self::new_unchecked(raw))
199 } else {
200 Err(ConversionError::NonFinite)
201 }
202 }
203
204 #[inline]
210 pub fn try_to_time(self) -> Result<Time<S>, ConversionError> {
211 R::try_into_time(self.raw, &TimeContext::new())
212 }
213
214 #[inline]
216 pub fn to_time_with(self, ctx: &TimeContext) -> Result<Time<S>, ConversionError> {
217 R::try_into_time(self.raw, ctx)
218 }
219}
220
221impl<S: Scale, R> EncodedTime<S, R>
222where
223 R: InfallibleRepresentationForScale<S>,
224{
225 #[inline]
226 pub(crate) fn from_time_infallible(time: Time<S>) -> Self {
227 Self::new_unchecked(R::from_time(time))
228 }
229
230 #[inline]
232 pub fn to_time(self) -> Time<S> {
233 R::into_time(self.raw)
234 }
235
236 #[allow(private_bounds)]
238 #[inline]
239 pub fn to<T>(self) -> T::Output
240 where
241 T: InfallibleConversionTarget<S>,
242 {
243 T::convert(self.to_time())
244 }
245
246 #[allow(private_bounds)]
248 #[inline]
249 pub fn try_to<T>(self) -> Result<T::Output, ConversionError>
250 where
251 T: ConversionTarget<S>,
252 {
253 T::try_convert(self.to_time())
254 }
255}
256
257impl<S: Scale, R> EncodedTime<S, R>
258where
259 R: RepresentationForScale<S>,
260{
261 #[allow(private_bounds)]
263 #[inline]
264 pub fn to_with<T>(self, ctx: &TimeContext) -> Result<T::Output, ConversionError>
265 where
266 T: ContextConversionTarget<S>,
267 {
268 T::convert_with(self.to_time_with(ctx)?, ctx)
269 }
270}
271
272pub type JulianDate<S> = EncodedTime<S, JD>;
274
275pub type ModifiedJulianDate<S> = EncodedTime<S, MJD>;
277
278pub type J2000Seconds<S> = EncodedTime<S, J2000s>;
280
281pub type UnixTime = EncodedTime<UTC, Unix>;
283
284pub type GpsTime = EncodedTime<TAI, GPS>;
286
287macro_rules! coordinate_representation {
288 ($repr:ty, $quantity:ty, $from_time:expr, $to_time:expr) => {
289 impl<S: CoordinateScale> RepresentationForScale<S> for $repr {
290 #[inline]
291 fn try_from_time(
292 time: Time<S>,
293 _ctx: &TimeContext,
294 ) -> Result<$quantity, ConversionError> {
295 Ok(<Self as InfallibleRepresentationForScale<S>>::from_time(
296 time,
297 ))
298 }
299
300 #[inline]
301 fn try_into_time(
302 raw: $quantity,
303 _ctx: &TimeContext,
304 ) -> Result<Time<S>, ConversionError> {
305 Ok(<Self as InfallibleRepresentationForScale<S>>::into_time(
306 raw,
307 ))
308 }
309 }
310
311 impl<S: CoordinateScale> InfallibleRepresentationForScale<S> for $repr {
312 #[inline]
313 fn from_time(time: Time<S>) -> $quantity {
314 $from_time(time)
315 }
316
317 #[inline]
318 fn into_time(raw: $quantity) -> Time<S> {
319 $to_time(raw)
320 }
321 }
322 };
323}
324
325coordinate_representation!(
326 J2000s,
327 Second,
328 |time: Time<_>| time.raw_j2000_seconds(),
329 |raw: Second| Time::from_raw_j2000_seconds(raw).expect("finite J2000 seconds must decode")
330);
331coordinate_representation!(
332 JD,
333 Day,
334 |time: Time<_>| j2000_seconds_to_jd(time.raw_j2000_seconds()),
335 |raw: Day| Time::from_raw_j2000_seconds(jd_to_j2000_seconds(raw))
336 .expect("finite Julian date must decode")
337);
338coordinate_representation!(
339 MJD,
340 Day,
341 |time: Time<_>| j2000_seconds_to_mjd(time.raw_j2000_seconds()),
342 |raw: Day| Time::from_raw_j2000_seconds(mjd_to_j2000_seconds(raw))
343 .expect("finite Modified Julian date must decode")
344);
345
346impl RepresentationForScale<UTC> for Unix {
347 #[inline]
348 fn try_from_time(time: Time<UTC>, ctx: &TimeContext) -> Result<Second, ConversionError> {
349 time.raw_unix_seconds_with(ctx)
350 }
351
352 #[inline]
353 fn try_into_time(raw: Second, ctx: &TimeContext) -> Result<Time<UTC>, ConversionError> {
354 Time::from_raw_unix_seconds_with(raw, ctx)
355 }
356}
357
358impl RepresentationForScale<TAI> for GPS {
359 #[inline]
360 fn try_from_time(time: Time<TAI>, _ctx: &TimeContext) -> Result<Second, ConversionError> {
361 Ok(<Self as InfallibleRepresentationForScale<TAI>>::from_time(
362 time,
363 ))
364 }
365
366 #[inline]
367 fn try_into_time(raw: Second, _ctx: &TimeContext) -> Result<Time<TAI>, ConversionError> {
368 Ok(<Self as InfallibleRepresentationForScale<TAI>>::into_time(
369 raw,
370 ))
371 }
372}
373
374impl InfallibleRepresentationForScale<TAI> for GPS {
375 #[inline]
376 fn from_time(time: Time<TAI>) -> Second {
377 time.raw_gps_seconds()
378 }
379
380 #[inline]
381 fn into_time(raw: Second) -> Time<TAI> {
382 Time::from_raw_gps_seconds(raw).expect("finite GPS seconds must decode")
383 }
384}
385
386impl<S: Scale, R> From<EncodedTime<S, R>> for Time<S>
387where
388 R: InfallibleRepresentationForScale<S>,
389{
390 #[inline]
391 fn from(value: EncodedTime<S, R>) -> Self {
392 value.to_time()
393 }
394}
395
396impl<S: Scale, R> From<Time<S>> for EncodedTime<S, R>
397where
398 R: InfallibleRepresentationForScale<S>,
399{
400 #[inline]
401 fn from(value: Time<S>) -> Self {
402 Self::from_time_infallible(value)
403 }
404}
405
406impl<S: CoordinateScale> ConversionTarget<S> for J2000s {
409 type Output = EncodedTime<S, J2000s>;
410
411 #[inline]
412 fn try_convert(src: Time<S>) -> Result<Self::Output, ConversionError> {
413 Ok(EncodedTime::from_time_infallible(src))
414 }
415}
416
417impl<S: CoordinateScale> InfallibleConversionTarget<S> for J2000s {
418 #[inline]
419 fn convert(src: Time<S>) -> Self::Output {
420 EncodedTime::from_time_infallible(src)
421 }
422}
423
424impl<S: CoordinateScale> ConversionTarget<S> for JD {
425 type Output = EncodedTime<S, JD>;
426
427 #[inline]
428 fn try_convert(src: Time<S>) -> Result<Self::Output, ConversionError> {
429 Ok(EncodedTime::from_time_infallible(src))
430 }
431}
432
433impl<S: CoordinateScale> InfallibleConversionTarget<S> for JD {
434 #[inline]
435 fn convert(src: Time<S>) -> Self::Output {
436 EncodedTime::from_time_infallible(src)
437 }
438}
439
440impl<S: CoordinateScale> ConversionTarget<S> for MJD {
441 type Output = EncodedTime<S, MJD>;
442
443 #[inline]
444 fn try_convert(src: Time<S>) -> Result<Self::Output, ConversionError> {
445 Ok(EncodedTime::from_time_infallible(src))
446 }
447}
448
449impl<S: CoordinateScale> InfallibleConversionTarget<S> for MJD {
450 #[inline]
451 fn convert(src: Time<S>) -> Self::Output {
452 EncodedTime::from_time_infallible(src)
453 }
454}
455
456impl<S> ConversionTarget<S> for Unix
457where
458 S: crate::scale::Scale + InfallibleScaleConvert<UTC>,
459{
460 type Output = EncodedTime<UTC, Unix>;
461
462 #[inline]
466 fn try_convert(src: Time<S>) -> Result<Self::Output, ConversionError> {
467 let utc = src.to_scale::<UTC>();
468 let raw = Unix::try_from_time(utc, &TimeContext::new())?;
469 Ok(EncodedTime::new_unchecked(raw))
470 }
471}
472
473impl ContextConversionTarget<UTC> for Unix {
474 type Output = EncodedTime<UTC, Unix>;
475
476 #[inline]
477 fn convert_with(src: Time<UTC>, ctx: &TimeContext) -> Result<Self::Output, ConversionError> {
478 let raw = Unix::try_from_time(src, ctx)?;
479 Ok(EncodedTime::new_unchecked(raw))
480 }
481}
482
483impl<S> ContextConversionTarget<S> for Unix
484where
485 S: crate::scale::Scale + crate::scale::conversion::ContextScaleConvert<UTC>,
486{
487 type Output = EncodedTime<UTC, Unix>;
488
489 #[inline]
490 fn convert_with(src: Time<S>, ctx: &TimeContext) -> Result<Self::Output, ConversionError> {
491 let utc = src.to_scale_with::<UTC>(ctx)?;
492 let raw = Unix::try_from_time(utc, ctx)?;
493 Ok(EncodedTime::new_unchecked(raw))
494 }
495}
496
497impl<S> ConversionTarget<S> for GPS
498where
499 S: crate::scale::Scale + InfallibleScaleConvert<TAI>,
500{
501 type Output = EncodedTime<TAI, GPS>;
502
503 #[inline]
504 fn try_convert(src: Time<S>) -> Result<Self::Output, ConversionError> {
505 Ok(Self::convert(src))
506 }
507}
508
509impl<S> InfallibleConversionTarget<S> for GPS
510where
511 S: crate::scale::Scale + InfallibleScaleConvert<TAI>,
512{
513 #[inline]
514 fn convert(src: Time<S>) -> Self::Output {
515 EncodedTime::from_time_infallible(src.to_scale::<TAI>())
516 }
517}
518
519impl<S> ContextConversionTarget<S> for GPS
520where
521 S: crate::scale::Scale + crate::scale::conversion::ContextScaleConvert<TAI>,
522{
523 type Output = EncodedTime<TAI, GPS>;
524
525 #[inline]
526 fn convert_with(src: Time<S>, ctx: &TimeContext) -> Result<Self::Output, ConversionError> {
527 let tai = src.to_scale_with::<TAI>(ctx)?;
528 Ok(EncodedTime::from_time_infallible(tai))
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use crate::scale::TT;
536
537 #[test]
538 fn encoded_time_display_delegates_to_quantity() {
539 let jd = JulianDate::<TT>::try_new(Day::new(2_451_545.123_456_789)).unwrap();
540
541 assert_eq!(format!("{jd:.9}"), "2451545.123456789 d");
542 }
543
544 #[test]
545 fn encoded_time_lower_exp_delegates_to_quantity() {
546 let seconds = J2000Seconds::<TT>::try_new(Second::new(1_234.5)).unwrap();
547 let formatted = format!("{seconds:.2e}");
548
549 assert!(formatted.contains("e"));
550 assert!(formatted.ends_with(" s"));
551 }
552}