pleiades_types/time.rs
1//! Time primitives: [`JulianDay`], [`TimeScale`], [`TimeScaleConversion`], and [`Instant`].
2
3use core::fmt;
4use core::time::Duration;
5
6use crate::angles::Angle;
7
8/// A Julian day expressed as a floating-point day count.
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
11pub struct JulianDay(f64);
12
13impl JulianDay {
14 /// Creates a new Julian day value.
15 pub const fn from_days(days: f64) -> Self {
16 Self(days)
17 }
18
19 /// Returns the raw floating-point day count.
20 pub const fn days(self) -> f64 {
21 self.0
22 }
23
24 /// Returns a Julian day shifted by the supplied number of SI seconds.
25 ///
26 /// This is a mechanical day-count operation. It does not choose or model a
27 /// time-scale conversion policy by itself; callers must provide the offset
28 /// appropriate for the source and target scales.
29 pub fn add_seconds(self, seconds: f64) -> Self {
30 Self(self.0 + seconds / SECONDS_PER_DAY)
31 }
32}
33
34impl fmt::Display for JulianDay {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 write!(f, "JD {}", self.0)
37 }
38}
39
40/// A supported astronomical time scale.
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
43#[non_exhaustive]
44pub enum TimeScale {
45 /// Coordinated Universal Time.
46 Utc,
47 /// Universal Time 1.
48 Ut1,
49 /// Terrestrial Time.
50 Tt,
51 /// Barycentric Dynamical Time.
52 Tdb,
53}
54
55impl fmt::Display for TimeScale {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 let label = match self {
58 Self::Utc => "UTC",
59 Self::Ut1 => "UT1",
60 Self::Tt => "TT",
61 Self::Tdb => "TDB",
62 };
63 f.write_str(label)
64 }
65}
66
67/// Number of SI seconds in one Julian day.
68pub const SECONDS_PER_DAY: f64 = 86_400.0;
69
70/// J2000.0 mean obliquity of the ecliptic, degrees (IAU 1976 constant term).
71/// Single source of truth shared by the SPK ICRF→ecliptic reduction
72/// (`pleiades-jpl`), the J2000→date precession (`pleiades-apparent`), and the
73/// constant term of [`Instant::mean_obliquity`].
74pub const OBLIQUITY_J2000_DEG: f64 = 23.439_291_111_111_11;
75
76/// Error returned when a caller-provided time-scale conversion fails.
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub enum TimeScaleConversionError {
79 /// Time scale required by the conversion helper.
80 Expected {
81 /// The time scale the conversion helper required.
82 expected: TimeScale,
83 /// The time scale actually supplied by the caller.
84 actual: TimeScale,
85 },
86 /// The supplied offset was not a finite number of seconds.
87 NonFiniteOffset,
88}
89
90impl TimeScaleConversionError {
91 pub(crate) const fn expected(expected: TimeScale, actual: TimeScale) -> Self {
92 Self::Expected { expected, actual }
93 }
94
95 pub(crate) const fn non_finite_offset() -> Self {
96 Self::NonFiniteOffset
97 }
98
99 /// Returns a compact one-line rendering of the conversion failure.
100 pub fn summary_line(&self) -> String {
101 match self {
102 Self::Expected { expected, actual } => format!(
103 "time-scale conversion expected {}, got {}",
104 expected, actual
105 ),
106 Self::NonFiniteOffset => "time-scale conversion offset must be finite".to_string(),
107 }
108 }
109}
110
111impl fmt::Display for TimeScaleConversionError {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 f.write_str(&self.summary_line())
114 }
115}
116
117impl std::error::Error for TimeScaleConversionError {}
118
119/// A caller-supplied time-scale conversion policy.
120///
121/// The conversion stores the source and target time scales plus the explicit
122/// `target - source` offset in SI seconds. It does not model Delta T,
123/// leap seconds, DUT1, or relativistic TDB terms itself; it only packages the
124/// caller's chosen rule so an instant can be retagged explicitly and
125/// reproducibly. Its compact summary renders the structured field names
126/// explicitly so release-facing diagnostics do not have to infer which side of
127/// the conversion the offset applies to.
128///
129/// # Example
130///
131/// ```
132/// use pleiades_types::{Instant, JulianDay, TimeScale, TimeScaleConversion};
133///
134/// let policy = TimeScaleConversion::new(TimeScale::Ut1, TimeScale::Tt, 64.184);
135/// let instant = Instant::new(JulianDay::from_days(2_451_545.0), TimeScale::Ut1);
136/// let converted = policy.apply(instant).expect("UT1-tagged instant");
137///
138/// assert_eq!(policy.summary_line(), "source=UT1; target=TT; offset_seconds=64.184 s");
139/// assert_eq!(converted.scale, TimeScale::Tt);
140/// ```
141///
142/// ```
143/// use pleiades_types::{Instant, JulianDay, TimeScale, TimeScaleConversion};
144///
145/// let policy = TimeScaleConversion::new(TimeScale::Tdb, TimeScale::Tt, -0.001_657);
146/// let instant = Instant::new(JulianDay::from_days(2_451_545.0), TimeScale::Tdb);
147///
148/// assert!(policy.validate(instant).is_ok());
149/// assert_eq!(policy.summary_line(), "source=TDB; target=TT; offset_seconds=-0.001657 s");
150/// assert_eq!(policy.validated_summary_line(instant).unwrap(), policy.summary_line());
151/// ```
152#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
153#[derive(Clone, Copy, Debug, PartialEq)]
154pub struct TimeScaleConversion {
155 /// The source time scale expected by the policy.
156 pub source: TimeScale,
157 /// The target time scale produced by the policy.
158 pub target: TimeScale,
159 /// The explicit `target - source` offset in SI seconds.
160 pub offset_seconds: f64,
161}
162
163impl TimeScaleConversion {
164 /// Creates a new caller-supplied time-scale conversion policy.
165 pub const fn new(source: TimeScale, target: TimeScale, offset_seconds: f64) -> Self {
166 Self {
167 source,
168 target,
169 offset_seconds,
170 }
171 }
172
173 /// Returns a compact one-line rendering of the caller-supplied policy.
174 pub fn summary_line(&self) -> String {
175 format!(
176 "source={}; target={}; offset_seconds={} s",
177 self.source, self.target, self.offset_seconds
178 )
179 }
180
181 /// Returns the compact summary line after validating the policy.
182 ///
183 /// This keeps the fail-closed rendering path co-located with the explicit
184 /// conversion contract when a caller wants to report the policy before
185 /// mutating the instant.
186 pub fn validated_summary_line(
187 &self,
188 instant: Instant,
189 ) -> Result<String, TimeScaleConversionError> {
190 self.validate(instant)?;
191 Ok(self.summary_line())
192 }
193
194 /// Validates the policy against a specific instant without retagging it.
195 ///
196 /// This is useful when a caller wants to preflight the explicit conversion
197 /// contract before mutating the instant itself. The source scale must match
198 /// the instant's current scale, and the offset must be finite.
199 pub fn validate(self, instant: Instant) -> Result<(), TimeScaleConversionError> {
200 if instant.scale != self.source {
201 return Err(TimeScaleConversionError::expected(
202 self.source,
203 instant.scale,
204 ));
205 }
206
207 checked_time_scale_offset(self.offset_seconds)?;
208 Ok(())
209 }
210
211 /// Applies the policy to an instant.
212 ///
213 /// This is the mutating counterpart to [`TimeScaleConversion::validate`].
214 /// The source scale must match the instant's current scale, and the offset
215 /// must be finite. Otherwise the conversion fails with the same structured
216 /// time-scale error used by the lower-level helpers.
217 pub fn apply(self, instant: Instant) -> Result<Instant, TimeScaleConversionError> {
218 self.validate(instant)?;
219 Ok(instant.with_time_scale_offset(self.target, self.offset_seconds))
220 }
221}
222
223impl fmt::Display for TimeScaleConversion {
224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225 f.write_str(&self.summary_line())
226 }
227}
228
229pub(crate) fn checked_time_scale_offset(
230 offset_seconds: f64,
231) -> Result<f64, TimeScaleConversionError> {
232 if offset_seconds.is_finite() {
233 Ok(offset_seconds)
234 } else {
235 Err(TimeScaleConversionError::non_finite_offset())
236 }
237}
238
239/// A Julian day tagged with a time scale.
240#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
241#[derive(Clone, Copy, Debug, PartialEq)]
242pub struct Instant {
243 /// The numeric Julian day value.
244 pub julian_day: JulianDay,
245 /// The time scale used by the Julian day value.
246 pub scale: TimeScale,
247}
248
249impl Instant {
250 /// Creates a new instant from a Julian day and time scale.
251 pub const fn new(julian_day: JulianDay, scale: TimeScale) -> Self {
252 Self { julian_day, scale }
253 }
254
255 /// Returns a compact one-line rendering of the instant.
256 pub fn summary_line(&self) -> String {
257 format!("{} {}", self.julian_day, self.scale)
258 }
259
260 /// Applies a caller-supplied time-scale conversion policy.
261 ///
262 /// This helper is the generic counterpart to the source-specific
263 /// `tt_from_*` / `tdb_from_*` methods. It lets callers package the explicit
264 /// source, target, and offset choice into one typed record when they want
265 /// to keep the conversion contract alongside the instant.
266 pub fn with_time_scale_conversion(
267 self,
268 conversion: TimeScaleConversion,
269 ) -> Result<Self, TimeScaleConversionError> {
270 conversion.apply(self)
271 }
272
273 /// Validates a caller-supplied time-scale conversion policy without retagging the instant.
274 ///
275 /// This is the foundation-layer counterpart to
276 /// [`TimeScaleConversion::validate`], which lets callers preflight the same
277 /// explicit source/target/offset contract directly from an instant when
278 /// they do not yet want to mutate it.
279 ///
280 /// # Example
281 ///
282 /// ```
283 /// use pleiades_types::{Instant, JulianDay, TimeScale, TimeScaleConversion};
284 ///
285 /// let instant = Instant::new(JulianDay::from_days(2_451_545.0), TimeScale::Ut1);
286 /// let policy = TimeScaleConversion::new(TimeScale::Ut1, TimeScale::Tt, 64.184);
287 ///
288 /// assert!(instant.validate_time_scale_conversion(policy).is_ok());
289 /// ```
290 pub fn validate_time_scale_conversion(
291 self,
292 conversion: TimeScaleConversion,
293 ) -> Result<(), TimeScaleConversionError> {
294 conversion.validate(self)
295 }
296
297 /// Returns this instant with a caller-supplied offset applied and a new time
298 /// scale tag.
299 ///
300 /// The offset is expressed as `target - source` in SI seconds. For example,
301 /// callers converting UT1 to TT can pass Delta T (`TT - UT1`) and set
302 /// `target_scale` to [`TimeScale::Tt`]. This helper intentionally performs
303 /// no leap-second, DUT1, Delta T, or relativistic modeling; it only makes the
304 /// caller-provided policy explicit and reproducible. Callers should pass a
305 /// finite offset; the validated signed helpers reject non-finite values
306 /// before reaching this low-level retagging step.
307 pub fn with_time_scale_offset(self, target_scale: TimeScale, offset_seconds: f64) -> Self {
308 Self {
309 julian_day: self.julian_day.add_seconds(offset_seconds),
310 scale: target_scale,
311 }
312 }
313
314 /// Returns this instant with a caller-supplied offset applied and a new time
315 /// scale tag after validating the source scale and offset.
316 ///
317 /// This is the checked counterpart to [`Instant::with_time_scale_offset`].
318 /// It keeps the same explicit `target - source` interpretation while
319 /// rejecting non-finite offsets and mismatched source scales before the
320 /// instant is retagged.
321 ///
322 /// # Example
323 ///
324 /// ```
325 /// use pleiades_types::{Instant, JulianDay, TimeScale};
326 ///
327 /// let instant = Instant::new(JulianDay::from_days(2_451_545.0), TimeScale::Ut1);
328 /// let converted = instant
329 /// .with_time_scale_offset_checked(TimeScale::Tt, 64.184)
330 /// .expect("validated offset");
331 ///
332 /// assert_eq!(converted.scale, TimeScale::Tt);
333 /// ```
334 pub fn with_time_scale_offset_checked(
335 self,
336 target_scale: TimeScale,
337 offset_seconds: f64,
338 ) -> Result<Self, TimeScaleConversionError> {
339 TimeScaleConversion::new(self.scale, target_scale, offset_seconds).apply(self)
340 }
341
342 /// Returns the mean obliquity of the ecliptic for this instant.
343 ///
344 /// The value uses the shared cubic approximation currently used throughout
345 /// the workspace for precession-era obliquity values. The backends'
346 /// J2000 equatorial channel does not use it; it rotates by
347 /// [`OBLIQUITY_J2000_DEG`](crate::OBLIQUITY_J2000_DEG) instead. It is
348 /// expressed as a typed angle so callers can pass it directly into
349 /// coordinate conversion helpers.
350 pub fn mean_obliquity(self) -> Angle {
351 let t = (self.julian_day.days() - 2_451_545.0) / 36_525.0;
352 Angle::from_degrees(
353 OBLIQUITY_J2000_DEG
354 - 0.013_004_166_666_666_667 * t
355 - 0.000_000_163_888_888_888_888_88 * t * t
356 + 0.000_000_503_611_111_111_111_1 * t * t * t,
357 )
358 }
359
360 /// Converts a UT1-tagged instant to TT using caller-supplied Delta T.
361 ///
362 /// `delta_t` must be the value `TT - UT1`. Use this when validation data or
363 /// an application already has an explicit Delta T policy and wants to pass a
364 /// TT instant to backends that require TT. UTC-to-TT conversion is not
365 /// represented by this helper, because UTC also requires leap-second and
366 /// DUT1 handling outside the current type layer.
367 ///
368 /// For signed `TT - UT1` policies, use [`Instant::tt_from_ut1_signed`].
369 ///
370 /// # Example
371 ///
372 /// ```
373 /// use std::time::Duration;
374 /// use pleiades_types::{Instant, JulianDay, TimeScale};
375 ///
376 /// let instant = Instant::new(JulianDay::from_days(2_451_545.0), TimeScale::Ut1);
377 /// let converted = instant.tt_from_ut1(Duration::from_secs_f64(64.184)).expect("UT1-tagged instant");
378 ///
379 /// assert_eq!(converted.scale, TimeScale::Tt);
380 /// assert!(converted.julian_day.days() > instant.julian_day.days());
381 /// ```
382 pub fn tt_from_ut1(self, delta_t: Duration) -> Result<Self, TimeScaleConversionError> {
383 if self.scale != TimeScale::Ut1 {
384 return Err(TimeScaleConversionError::expected(
385 TimeScale::Ut1,
386 self.scale,
387 ));
388 }
389
390 Ok(self.with_time_scale_offset(TimeScale::Tt, delta_t.as_secs_f64()))
391 }
392
393 /// Converts a UT1-tagged instant to TT using a caller-supplied signed offset.
394 ///
395 /// `offset_seconds` must be the already-chosen signed `TT - UT1` offset in
396 /// SI seconds. The helper intentionally does not model leap seconds or
397 /// DUT1 by itself; it only makes a caller-supplied UT1-to-TT policy explicit
398 /// and reproducible for applications that need a TT-tagged request surface.
399 ///
400 /// # Example
401 ///
402 /// ```
403 /// use pleiades_types::{Instant, JulianDay, TimeScale};
404 ///
405 /// let instant = Instant::new(JulianDay::from_days(2_451_545.0), TimeScale::Ut1);
406 /// let converted = instant.tt_from_ut1_signed(64.184).expect("UT1-tagged instant");
407 ///
408 /// assert_eq!(converted.scale, TimeScale::Tt);
409 /// assert!(converted.julian_day.days() > instant.julian_day.days());
410 /// ```
411 pub fn tt_from_ut1_signed(self, offset_seconds: f64) -> Result<Self, TimeScaleConversionError> {
412 if self.scale != TimeScale::Ut1 {
413 return Err(TimeScaleConversionError::expected(
414 TimeScale::Ut1,
415 self.scale,
416 ));
417 }
418
419 let offset_seconds = checked_time_scale_offset(offset_seconds)?;
420
421 Ok(self.with_time_scale_offset(TimeScale::Tt, offset_seconds))
422 }
423
424 /// Converts a UTC-tagged instant to TT using caller-supplied offset.
425 ///
426 /// `delta_t` must be the already-chosen `TT - UTC` offset in SI seconds.
427 /// The helper intentionally does not model leap seconds or DUT1 by itself;
428 /// it only makes a caller-supplied UTC-to-TT policy explicit and
429 /// reproducible for applications that start from civil time.
430 ///
431 /// For signed `TT - UTC` policies, use [`Instant::tt_from_utc_signed`].
432 pub fn tt_from_utc(self, delta_t: Duration) -> Result<Self, TimeScaleConversionError> {
433 if self.scale != TimeScale::Utc {
434 return Err(TimeScaleConversionError::expected(
435 TimeScale::Utc,
436 self.scale,
437 ));
438 }
439
440 Ok(self.with_time_scale_offset(TimeScale::Tt, delta_t.as_secs_f64()))
441 }
442
443 /// Converts a UTC-tagged instant to TT using a caller-supplied signed offset.
444 ///
445 /// `offset_seconds` must be the already-chosen signed `TT - UTC` offset in
446 /// SI seconds. The helper intentionally does not model leap seconds or
447 /// DUT1 by itself; it only makes a caller-supplied UTC-to-TT policy explicit
448 /// and reproducible for applications that need a TT-tagged request surface.
449 ///
450 /// # Example
451 ///
452 /// ```
453 /// use pleiades_types::{Instant, JulianDay, TimeScale};
454 ///
455 /// let instant = Instant::new(JulianDay::from_days(2_451_545.0), TimeScale::Utc);
456 /// let converted = instant.tt_from_utc_signed(64.184).expect("UTC-tagged instant");
457 ///
458 /// assert_eq!(converted.scale, TimeScale::Tt);
459 /// assert!(converted.julian_day.days() > instant.julian_day.days());
460 /// ```
461 pub fn tt_from_utc_signed(self, offset_seconds: f64) -> Result<Self, TimeScaleConversionError> {
462 if self.scale != TimeScale::Utc {
463 return Err(TimeScaleConversionError::expected(
464 TimeScale::Utc,
465 self.scale,
466 ));
467 }
468
469 let offset_seconds = checked_time_scale_offset(offset_seconds)?;
470
471 Ok(self.with_time_scale_offset(TimeScale::Tt, offset_seconds))
472 }
473
474 /// Converts a TT-tagged instant to TDB using a caller-supplied offset.
475 ///
476 /// `offset` must be the already-chosen `TDB - TT` offset in SI seconds.
477 /// For signed TDB-TT policies, use [`Instant::tdb_from_tt_signed`]. The
478 /// helper intentionally does not model relativistic terms by itself; it
479 /// only makes a caller-supplied TT-to-TDB policy explicit and reproducible
480 /// for applications that need a TDB-tagged request surface.
481 pub fn tdb_from_tt(self, offset: Duration) -> Result<Self, TimeScaleConversionError> {
482 if self.scale != TimeScale::Tt {
483 return Err(TimeScaleConversionError::expected(
484 TimeScale::Tt,
485 self.scale,
486 ));
487 }
488
489 Ok(self.with_time_scale_offset(TimeScale::Tdb, offset.as_secs_f64()))
490 }
491
492 /// Converts a TT-tagged instant to TDB using a caller-supplied signed offset.
493 ///
494 /// `offset_seconds` must be the already-chosen signed `TDB - TT` offset in
495 /// SI seconds. The helper intentionally does not model relativistic terms by
496 /// itself; it only makes a caller-supplied TT-to-TDB policy explicit and
497 /// reproducible for applications that need a TDB-tagged request surface.
498 pub fn tdb_from_tt_signed(self, offset_seconds: f64) -> Result<Self, TimeScaleConversionError> {
499 if self.scale != TimeScale::Tt {
500 return Err(TimeScaleConversionError::expected(
501 TimeScale::Tt,
502 self.scale,
503 ));
504 }
505
506 let offset_seconds = checked_time_scale_offset(offset_seconds)?;
507
508 Ok(self.with_time_scale_offset(TimeScale::Tdb, offset_seconds))
509 }
510
511 /// Converts a TDB-tagged instant to TT using a caller-supplied signed offset.
512 ///
513 /// `offset_seconds` must be the already-chosen signed `TT - TDB` offset in
514 /// SI seconds. The helper intentionally does not model relativistic terms by
515 /// itself; it only makes a caller-supplied TDB-to-TT policy explicit and
516 /// reproducible for applications that need a TT-tagged request surface.
517 pub fn tt_from_tdb(self, offset_seconds: f64) -> Result<Self, TimeScaleConversionError> {
518 if self.scale != TimeScale::Tdb {
519 return Err(TimeScaleConversionError::expected(
520 TimeScale::Tdb,
521 self.scale,
522 ));
523 }
524
525 let offset_seconds = checked_time_scale_offset(offset_seconds)?;
526
527 Ok(self.with_time_scale_offset(TimeScale::Tt, offset_seconds))
528 }
529
530 /// Converts a TDB-tagged instant to TT using a caller-supplied signed offset.
531 ///
532 /// This is an explicit alias for [`Instant::tt_from_tdb`] that mirrors the
533 /// signed helper naming used for the other time-scale conversion policies.
534 ///
535 /// # Example
536 ///
537 /// ```
538 /// use pleiades_types::{Instant, JulianDay, TimeScale};
539 ///
540 /// let instant = Instant::new(JulianDay::from_days(2_451_545.0), TimeScale::Tdb);
541 /// let converted = instant.tt_from_tdb_signed(-0.001_657).expect("TDB-tagged instant");
542 ///
543 /// assert_eq!(converted.scale, TimeScale::Tt);
544 /// assert!(converted.julian_day.days() < instant.julian_day.days());
545 /// ```
546 pub fn tt_from_tdb_signed(self, offset_seconds: f64) -> Result<Self, TimeScaleConversionError> {
547 self.tt_from_tdb(offset_seconds)
548 }
549
550 /// Converts a UT1-tagged instant to TDB using caller-supplied TT-UT1 and
551 /// TDB-TT offsets.
552 ///
553 /// `tt_offset` must be the already-chosen `TT - UT1` offset in SI
554 /// seconds. `tdb_offset` must be the already-chosen `TDB - TT` offset in
555 /// SI seconds. The helper intentionally does not model leap seconds,
556 /// DUT1, or relativistic terms by itself; it only composes caller-supplied
557 /// policy steps into a reproducible TDB-tagged instant.
558 pub fn tdb_from_ut1(
559 self,
560 tt_offset: Duration,
561 tdb_offset: Duration,
562 ) -> Result<Self, TimeScaleConversionError> {
563 let tt = self.tt_from_ut1(tt_offset)?;
564 tt.tdb_from_tt(tdb_offset)
565 }
566
567 /// Converts a UT1-tagged instant to TDB using caller-supplied TT-UT1 and
568 /// signed TDB-TT offsets.
569 ///
570 /// `tt_offset` must be the already-chosen `TT - UT1` offset in SI
571 /// seconds. `tdb_offset_seconds` must be the already-chosen signed
572 /// `TDB - TT` offset in SI seconds. The helper intentionally does not
573 /// model leap seconds, DUT1, or relativistic terms by itself; it only
574 /// composes caller-supplied policy steps into a reproducible TDB-tagged
575 /// instant.
576 ///
577 /// # Example
578 ///
579 /// ```
580 /// use std::time::Duration;
581 /// use pleiades_types::{Instant, JulianDay, TimeScale};
582 ///
583 /// let instant = Instant::new(JulianDay::from_days(2_451_545.0), TimeScale::Ut1);
584 /// let converted = instant
585 /// .tdb_from_ut1_signed(Duration::from_secs_f64(64.184), -0.001_657)
586 /// .expect("UT1-tagged instant");
587 ///
588 /// assert_eq!(converted.scale, TimeScale::Tdb);
589 /// assert!(converted.julian_day.days() > instant.julian_day.days());
590 /// ```
591 pub fn tdb_from_ut1_signed(
592 self,
593 tt_offset: Duration,
594 tdb_offset_seconds: f64,
595 ) -> Result<Self, TimeScaleConversionError> {
596 let tt = self.tt_from_ut1(tt_offset)?;
597 tt.tdb_from_tt_signed(tdb_offset_seconds)
598 }
599
600 /// Converts a UTC-tagged instant to TDB using caller-supplied TT-UTC and
601 /// TDB-TT offsets.
602 ///
603 /// `tt_offset` must be the already-chosen `TT - UTC` offset in SI seconds.
604 /// `tdb_offset` must be the already-chosen `TDB - TT` offset in SI
605 /// seconds. The helper intentionally does not model leap seconds, DUT1, or
606 /// relativistic terms by itself; it only composes caller-supplied policy
607 /// steps into a reproducible TDB-tagged instant.
608 pub fn tdb_from_utc(
609 self,
610 tt_offset: Duration,
611 tdb_offset: Duration,
612 ) -> Result<Self, TimeScaleConversionError> {
613 let tt = self.tt_from_utc(tt_offset)?;
614 tt.tdb_from_tt(tdb_offset)
615 }
616
617 /// Converts a UTC-tagged instant to TDB using caller-supplied TT-UTC and
618 /// signed TDB-TT offsets.
619 ///
620 /// `tt_offset` must be the already-chosen `TT - UTC` offset in SI seconds.
621 /// `tdb_offset_seconds` must be the already-chosen signed `TDB - TT`
622 /// offset in SI seconds. The helper intentionally does not model leap
623 /// seconds, DUT1, or relativistic terms by itself; it only composes
624 /// caller-supplied policy steps into a reproducible TDB-tagged instant.
625 ///
626 /// # Example
627 ///
628 /// ```
629 /// use std::time::Duration;
630 /// use pleiades_types::{Instant, JulianDay, TimeScale};
631 ///
632 /// let instant = Instant::new(JulianDay::from_days(2_451_545.0), TimeScale::Utc);
633 /// let converted = instant
634 /// .tdb_from_utc_signed(Duration::from_secs_f64(64.184), -0.001_657)
635 /// .expect("UTC-tagged instant");
636 ///
637 /// assert_eq!(converted.scale, TimeScale::Tdb);
638 /// assert!(converted.julian_day.days() > instant.julian_day.days());
639 /// ```
640 pub fn tdb_from_utc_signed(
641 self,
642 tt_offset: Duration,
643 tdb_offset_seconds: f64,
644 ) -> Result<Self, TimeScaleConversionError> {
645 let tt = self.tt_from_utc(tt_offset)?;
646 tt.tdb_from_tt_signed(tdb_offset_seconds)
647 }
648}
649
650impl fmt::Display for Instant {
651 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
652 f.write_str(&self.summary_line())
653 }
654}