1use core::fmt;
40use core::marker::PhantomData;
41use core::ops::{Add, Neg, Sub};
42
43use crate::error::ConversionError;
44use crate::format::{EncodedTime, TimeFormat};
45use crate::scale::Scale;
46use qtty::Quantity;
47
48pub struct Coord<S: Scale, F: TimeFormat> {
58 raw: Quantity<F::Unit>,
59 _marker: PhantomData<fn() -> S>,
60}
61
62pub struct Offset<S: Scale, F: TimeFormat> {
64 raw: Quantity<F::Unit>,
65 _marker: PhantomData<fn() -> S>,
66}
67
68macro_rules! impl_zst_plumbing {
71 ($ty:ident, $kind:literal) => {
72 impl<S: Scale, F: TimeFormat> Copy for $ty<S, F> {}
73
74 impl<S: Scale, F: TimeFormat> Clone for $ty<S, F> {
75 #[inline]
76 fn clone(&self) -> Self {
77 *self
78 }
79 }
80
81 impl<S: Scale, F: TimeFormat> PartialEq for $ty<S, F> {
82 #[inline]
83 fn eq(&self, other: &Self) -> bool {
84 self.raw == other.raw
85 }
86 }
87
88 impl<S: Scale, F: TimeFormat> PartialOrd for $ty<S, F> {
89 #[inline]
90 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
91 self.raw.partial_cmp(&other.raw)
92 }
93 }
94
95 impl<S: Scale, F: TimeFormat> fmt::Debug for $ty<S, F> {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 f.debug_struct($kind)
98 .field("scale", &S::NAME)
99 .field("format", &F::NAME)
100 .field("raw", &self.raw)
101 .finish()
102 }
103 }
104
105 impl<S: Scale, F: TimeFormat> fmt::Display for $ty<S, F>
106 where
107 qtty::Quantity<F::Unit>: fmt::Display,
108 {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 fmt::Display::fmt(&self.raw, f)
111 }
112 }
113
114 impl<S: Scale, F: TimeFormat> fmt::LowerExp for $ty<S, F>
115 where
116 qtty::Quantity<F::Unit>: fmt::LowerExp,
117 {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 fmt::LowerExp::fmt(&self.raw, f)
120 }
121 }
122
123 impl<S: Scale, F: TimeFormat> fmt::UpperExp for $ty<S, F>
124 where
125 qtty::Quantity<F::Unit>: fmt::UpperExp,
126 {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 fmt::UpperExp::fmt(&self.raw, f)
129 }
130 }
131
132 impl<S: Scale, F: TimeFormat> $ty<S, F> {
133 #[inline]
138 pub const fn from_raw_unchecked(raw: Quantity<F::Unit>) -> Self {
139 Self {
140 raw,
141 _marker: PhantomData,
142 }
143 }
144
145 #[inline]
147 pub const fn raw(self) -> Quantity<F::Unit> {
148 self.raw
149 }
150
151 #[inline]
153 pub const fn quantity(self) -> Quantity<F::Unit> {
154 self.raw
155 }
156 }
157 };
158}
159
160impl_zst_plumbing!(Coord, "Coord");
161impl_zst_plumbing!(Offset, "Offset");
162
163impl<S: Scale, F: TimeFormat> Coord<S, F> {
166 #[inline]
168 pub fn try_new(raw: Quantity<F::Unit>) -> Result<Self, ConversionError> {
169 if raw.is_finite() {
170 Ok(Self::from_raw_unchecked(raw))
171 } else {
172 Err(ConversionError::NonFinite)
173 }
174 }
175}
176
177impl<S: Scale, F: TimeFormat> Offset<S, F> {
178 #[inline]
180 pub fn try_new(raw: Quantity<F::Unit>) -> Result<Self, ConversionError> {
181 if raw.is_finite() {
182 Ok(Self::from_raw_unchecked(raw))
183 } else {
184 Err(ConversionError::NonFinite)
185 }
186 }
187
188 #[inline]
190 pub fn zero() -> Self
191 where
192 Quantity<F::Unit>: Default,
193 {
194 Self::from_raw_unchecked(Quantity::<F::Unit>::default())
195 }
196}
197
198impl<S: Scale, F: TimeFormat> Sub for Coord<S, F>
201where
202 Quantity<F::Unit>: Sub<Output = Quantity<F::Unit>>,
203{
204 type Output = Offset<S, F>;
205
206 #[inline]
207 fn sub(self, rhs: Self) -> Self::Output {
208 Offset::from_raw_unchecked(self.raw - rhs.raw)
209 }
210}
211
212impl<S: Scale, F: TimeFormat> Add<Offset<S, F>> for Coord<S, F>
213where
214 Quantity<F::Unit>: Add<Output = Quantity<F::Unit>>,
215{
216 type Output = Coord<S, F>;
217
218 #[inline]
219 fn add(self, rhs: Offset<S, F>) -> Self::Output {
220 Coord::from_raw_unchecked(self.raw + rhs.raw)
221 }
222}
223
224impl<S: Scale, F: TimeFormat> Sub<Offset<S, F>> for Coord<S, F>
225where
226 Quantity<F::Unit>: Sub<Output = Quantity<F::Unit>>,
227{
228 type Output = Coord<S, F>;
229
230 #[inline]
231 fn sub(self, rhs: Offset<S, F>) -> Self::Output {
232 Coord::from_raw_unchecked(self.raw - rhs.raw)
233 }
234}
235
236impl<S: Scale, F: TimeFormat> Add for Offset<S, F>
237where
238 Quantity<F::Unit>: Add<Output = Quantity<F::Unit>>,
239{
240 type Output = Self;
241
242 #[inline]
243 fn add(self, rhs: Self) -> Self::Output {
244 Self::from_raw_unchecked(self.raw + rhs.raw)
245 }
246}
247
248impl<S: Scale, F: TimeFormat> Sub for Offset<S, F>
249where
250 Quantity<F::Unit>: Sub<Output = Quantity<F::Unit>>,
251{
252 type Output = Self;
253
254 #[inline]
255 fn sub(self, rhs: Self) -> Self::Output {
256 Self::from_raw_unchecked(self.raw - rhs.raw)
257 }
258}
259
260impl<S: Scale, F: TimeFormat> Neg for Offset<S, F>
261where
262 Quantity<F::Unit>: Neg<Output = Quantity<F::Unit>>,
263{
264 type Output = Self;
265
266 #[inline]
267 fn neg(self) -> Self::Output {
268 Self::from_raw_unchecked(-self.raw)
269 }
270}
271
272impl<S: Scale, F: TimeFormat> From<Coord<S, F>> for EncodedTime<S, F> {
275 #[inline]
276 fn from(value: Coord<S, F>) -> Self {
277 EncodedTime::<S, F>::from_raw_unchecked(value.raw)
278 }
279}
280
281impl<S: Scale, F: TimeFormat> From<EncodedTime<S, F>> for Coord<S, F> {
282 #[inline]
283 fn from(value: EncodedTime<S, F>) -> Self {
284 Coord::<S, F>::from_raw_unchecked(value.raw())
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::format::{JD, MJD};
292 use crate::scale::{TT, UTC};
293 use qtty::Day;
294
295 #[test]
296 fn coord_round_trip_with_encoded_time() {
297 let c = Coord::<TT, JD>::try_new(Day::new(2_451_545.5)).unwrap();
298 let e: EncodedTime<TT, JD> = c.into();
299 let back: Coord<TT, JD> = e.into();
300 assert_eq!(c, back);
301 }
302
303 #[test]
304 fn coord_minus_coord_yields_offset() {
305 let a = Coord::<TT, JD>::from_raw_unchecked(Day::new(2_451_545.5));
306 let b = Coord::<TT, JD>::from_raw_unchecked(Day::new(2_451_545.0));
307 let v: Offset<TT, JD> = a - b;
308 assert_eq!(v.raw(), Day::new(0.5));
309 }
310
311 #[test]
312 fn coord_plus_offset_yields_coord() {
313 let a = Coord::<TT, JD>::from_raw_unchecked(Day::new(2_451_545.0));
314 let v = Offset::<TT, JD>::from_raw_unchecked(Day::new(1.5));
315 let b = a + v;
316 assert_eq!(b.raw(), Day::new(2_451_546.5));
317 }
318
319 #[test]
320 fn offset_arithmetic() {
321 let v = Offset::<TT, JD>::from_raw_unchecked(Day::new(1.0));
322 let w = Offset::<TT, JD>::from_raw_unchecked(Day::new(0.25));
323 assert_eq!((v + w).raw(), Day::new(1.25));
324 assert_eq!((v - w).raw(), Day::new(0.75));
325 assert_eq!((-v).raw(), Day::new(-1.0));
326 }
327
328 #[test]
329 fn try_new_rejects_non_finite() {
330 let nan = Coord::<TT, JD>::try_new(Day::new(f64::NAN));
331 assert!(matches!(nan, Err(ConversionError::NonFinite)));
332 let inf = Offset::<UTC, MJD>::try_new(Day::new(f64::INFINITY));
333 assert!(matches!(inf, Err(ConversionError::NonFinite)));
334 }
335
336 #[test]
337 fn debug_includes_scale_and_format() {
338 let c = Coord::<TT, JD>::from_raw_unchecked(Day::new(2_451_545.0));
339 let dbg = format!("{c:?}");
340 assert!(dbg.contains("TT"));
341 assert!(dbg.contains("JD"));
342 }
343
344 #[test]
345 fn display_delegates_to_quantity() {
346 let c = Coord::<TT, JD>::from_raw_unchecked(Day::new(2_451_545.5));
347 assert_eq!(format!("{c:.1}"), "2451545.5 d");
348 }
349
350 #[test]
351 fn coord_scale_phantom_prevents_mixing() {
352 fn accept_tt_jd(c: Coord<TT, JD>) -> Day {
353 c.raw()
354 }
355 fn accept_utc_jd(c: Coord<UTC, JD>) -> Day {
356 c.raw()
357 }
358
359 let tt = Coord::<TT, JD>::from_raw_unchecked(Day::new(2_451_545.0));
360 let utc = Coord::<UTC, JD>::from_raw_unchecked(Day::new(2_451_545.0));
361
362 let _ = accept_tt_jd(tt);
363 let _ = accept_utc_jd(utc);
364 }
365
366 #[test]
367 fn coord_quantity_is_alias_for_raw() {
368 let c = Coord::<TT, JD>::from_raw_unchecked(Day::new(2_451_545.5));
369 assert_eq!(c.raw(), c.quantity());
370 }
371
372 #[test]
373 fn offset_quantity_is_alias_for_raw() {
374 let v = Offset::<TT, JD>::from_raw_unchecked(Day::new(0.5));
375 assert_eq!(v.raw(), v.quantity());
376 }
377
378 #[test]
379 fn coord_minus_offset_yields_coord() {
380 let a = Coord::<TT, JD>::from_raw_unchecked(Day::new(2_451_546.0));
381 let v = Offset::<TT, JD>::from_raw_unchecked(Day::new(1.0));
382 let b = a - v;
383 assert_eq!(b.raw(), Day::new(2_451_545.0));
384 }
385
386 #[test]
387 fn coord_lower_exp_delegates_to_quantity() {
388 let c = Coord::<TT, JD>::from_raw_unchecked(Day::new(2_451_545.5));
389 assert_eq!(format!("{c:.2e}"), format!("{:.2e}", c.raw()));
390 }
391
392 #[test]
393 fn coord_upper_exp_delegates_to_quantity() {
394 let c = Coord::<TT, JD>::from_raw_unchecked(Day::new(2_451_545.5));
395 assert_eq!(format!("{c:.2E}"), format!("{:.2E}", c.raw()));
396 }
397
398 #[test]
399 fn offset_lower_exp_delegates_to_quantity() {
400 let v = Offset::<TT, JD>::from_raw_unchecked(Day::new(1.5));
401 assert_eq!(format!("{v:.2e}"), format!("{:.2e}", v.raw()));
402 }
403
404 #[test]
405 fn offset_upper_exp_delegates_to_quantity() {
406 let v = Offset::<TT, JD>::from_raw_unchecked(Day::new(1.5));
407 assert_eq!(format!("{v:.2E}"), format!("{:.2E}", v.raw()));
408 }
409
410 #[test]
411 fn offset_debug_includes_scale_and_format() {
412 let v = Offset::<TT, JD>::from_raw_unchecked(Day::new(1.0));
413 let dbg = format!("{v:?}");
414 assert!(dbg.contains("TT"));
415 assert!(dbg.contains("JD"));
416 }
417}