Skip to main content

deep_time/dt/
constructors.rs

1use crate::{
2    ATTOS_PER_FS, ATTOS_PER_MS, ATTOS_PER_NS, ATTOS_PER_PS, ATTOS_PER_SEC, ATTOS_PER_SEC_I128,
3    ATTOS_PER_US, Dt, Real, SEC_PER_DAYI64, SEC_PER_WEEK, Scale,
4    TAI_SECS_1970_MIDNIGHT_TO_2000_NOON,
5};
6
7impl Dt {
8    /// The library’s internal reference epoch: exactly **2000-01-01 12:00:00 TAI**.
9    ///
10    /// [`Dt::new(0, 0)`].
11    pub const ZERO: Self = Self::new(0, 0);
12
13    /// UNIX epoch.
14    /// - 1970-01-01 midnight TAI.
15    /// - Stored here on the **TAI** timescale as an offset from [`Self::ZERO`].
16    /// - -946_728_000 sec
17    /// - 0 attos
18    /// - The library's epoch for time scales during conversions is 2000-01-01 noon.
19    ///   This const is provided as a convenience.
20    pub const UNIX_EPOCH: Self = Self::new(-TAI_SECS_1970_MIDNIGHT_TO_2000_NOON, 0);
21
22    /// TT/TCG/TCB/TDB epoch.
23    /// - 1977-01-01 midnight TAI.
24    /// - Stored here on the **TAI** timescale as an offset from [`Self::ZERO`].
25    /// - -725_803_200 sec
26    /// - 0 attos
27    /// - The library's epoch for time scales during conversions is 2000-01-01 noon.
28    ///   This const is provided as a convenience.
29    pub const TAI_1977_EPOCH: Self = Self::new(-725_803_200, 0);
30
31    /// TT/TCG/TCB/TDB/TCL epoch.
32    /// - 1977-01-01 midnight TAI.
33    /// - Stored here on the **TCL** timescale as an offset from [`Self::ZERO`].
34    /// - The library's epoch for time scales during conversions is 2000-01-01 noon.
35    ///   This const is provided as a convenience.
36    pub const TCL_1977_EPOCH: Self = { Self::TAI_1977_EPOCH.to(Scale::TAI, Scale::TCL) };
37
38    /// Chandra X-ray Center (CXC) Time epoch.
39    /// - 1998-01-01 midnight TT.
40    /// - Stored here on the **TAI** timescale as an offset from [`Self::ZERO`].
41    /// - -63_115_233 sec
42    /// - 816000000000000000 attos
43    /// - The library's epoch for time scales during conversions is 2000-01-01 noon.
44    ///   This const is provided as a convenience.
45    pub const CXC_EPOCH: Self = Self::new(-63_115_233, 816000000000000000);
46
47    /// GPS/Galileo Experiment (GALEX) Time epoch.
48    /// - **1980-01-06 00:00:00 UTC**.
49    /// - Stored here on the **TAI** timescale as an offset from [`Self::ZERO`].
50    /// - -630_763_200 **+ 19** sec
51    /// - 0 attos
52    /// - The library's epoch for time scales during conversions is 2000-01-01 noon.
53    ///   This const is provided as a convenience.
54    pub const GPS_EPOCH: Self = Self::new(-630_763_200 + 19, 0);
55
56    /// Galileo System Time (GST) epoch.
57    /// - 1999-08-22 00:00:00 GST.
58    /// - Stored here on the **TAI** timescale as an offset from [`Self::ZERO`].
59    /// - -11_448_000 **+ 19** sec
60    /// - 0 attos
61    /// - The library's epoch for time scales during conversions is 2000-01-01 noon.
62    ///   This const is provided as a convenience.
63    pub const GALILEO_EPOCH: Self = Self::new(-11_448_000 + 19, 0);
64
65    /// BeiDou Time (BDT) epoch.
66    /// - 2006-01-01 00:00:00 UTC.
67    /// - Stored here on the **TAI** timescale as an offset from [`Self::ZERO`].
68    /// - 189_345_600 **+ 33** sec
69    /// - 0 attos
70    /// - The library's epoch for time scales during conversions is 2000-01-01 noon.
71    ///   This const is provided as a convenience.
72    pub const BDT_EPOCH: Self = Self::new(189_345_600 + 33, 0);
73
74    /// Maximum representable duration (`i64::MAX` seconds + 999... attoseconds).
75    pub const MAX: Self = Self {
76        sec: i64::MAX,
77        attos: ATTOS_PER_SEC - 1,
78    };
79
80    /// Minimum (most negative) representable duration (`i64::MIN` seconds).
81    pub const MIN: Self = Self {
82        sec: i64::MIN,
83        attos: 0,
84    };
85
86    pub const SEC_19: Self = Self::new(19, 0);
87    pub const SEC_33: Self = Self::new(33, 0);
88    pub const SEC_37: Self = Self::new(37, 0);
89    pub const ONE_DAY: Self = Self::new(SEC_PER_DAYI64, 0);
90
91    /// Creates a new `Dt` from whole seconds, a subsecond part in attoseconds,
92    /// and a scale, automatically normalizing the representation.
93    #[inline]
94    pub const fn new(sec: i64, attos: u64) -> Self {
95        let mut tp = Self { sec, attos };
96        tp.carry_over_mut();
97        tp
98    }
99
100    #[inline]
101    pub const fn from_attos(attos: i128, scale: Scale) -> Self {
102        Self::from_dt(Dt::attos_to_dt(attos), scale)
103    }
104
105    #[inline]
106    pub const fn from_sec(sec: i64, scale: Scale) -> Self {
107        Self::from(sec, 0, scale)
108    }
109
110    #[inline]
111    pub const fn from_ms(ms: i128, scale: Scale) -> Self {
112        let attos = ms.saturating_mul(ATTOS_PER_MS as i128);
113        Self::from_dt(Dt::attos_to_dt(attos), scale)
114    }
115
116    #[inline]
117    pub const fn from_us(us: i128, scale: Scale) -> Self {
118        let attos = us.saturating_mul(ATTOS_PER_US as i128);
119        Self::from_dt(Dt::attos_to_dt(attos), scale)
120    }
121
122    #[inline]
123    pub const fn from_ns(ns: i128, scale: Scale) -> Self {
124        let attos = ns.saturating_mul(ATTOS_PER_NS as i128);
125        Self::from_dt(Dt::attos_to_dt(attos), scale)
126    }
127
128    #[inline]
129    pub const fn from_ps(ps: i128, scale: Scale) -> Self {
130        let attos = ps.saturating_mul(ATTOS_PER_PS as i128);
131        Self::from_dt(Dt::attos_to_dt(attos), scale)
132    }
133
134    #[inline]
135    pub const fn from_fs(fs: i128, scale: Scale) -> Self {
136        let attos = fs.saturating_mul(ATTOS_PER_FS as i128);
137        Self::from_dt(Dt::attos_to_dt(attos), scale)
138    }
139
140    #[inline]
141    pub const fn from_min(m: i64, scale: Scale) -> Self {
142        Self::from(m * 60, 0, scale)
143    }
144
145    #[inline]
146    pub const fn from_hr(h: i64, scale: Scale) -> Self {
147        Self::from(h * 3600, 0, scale)
148    }
149
150    /// Creates a `Dt` from hours, minutes, seconds, milliseconds, microseconds,
151    /// and nanoseconds on the supplied scale.
152    pub const fn from_hms(
153        hr: i64,
154        min: i64,
155        sec: i64,
156        ms: i128,
157        us: i128,
158        ns: i128,
159        scale: Scale,
160    ) -> Self {
161        let total_sec = hr * 3600i64 + min * 60i64 + sec;
162
163        let sub_ns = ms * 1_000_000i128 + us * 1_000i128 + ns;
164
165        if sub_ns == 0 {
166            return Self::from(total_sec, 0, scale);
167        }
168
169        let abs_ns = sub_ns.unsigned_abs();
170        let extra_sec = (abs_ns / 1_000_000_000u128) as i64;
171        let rem_ns = abs_ns % 1_000_000_000u128;
172        let frac = (rem_ns as u64) * ATTOS_PER_NS;
173
174        let (final_sec, final_frac) = if sub_ns >= 0 {
175            (total_sec + extra_sec, frac)
176        } else if frac == 0 {
177            (total_sec - extra_sec, 0)
178        } else {
179            (total_sec - extra_sec - 1, ATTOS_PER_SEC - frac)
180        };
181
182        Self::from(final_sec, final_frac, scale)
183    }
184
185    pub(crate) const fn attos_to_dt(attos: i128) -> Self {
186        let q = safe_div_euc!(attos, ATTOS_PER_SEC_I128, 0i128);
187
188        if q > (i64::MAX as i128) {
189            Self::MAX
190        } else if q < (i64::MIN as i128) {
191            Self::MIN
192        } else {
193            let r = safe_rem_euc!(attos, ATTOS_PER_SEC_I128, 0i128);
194            Self {
195                sec: q as i64,
196                attos: r as u64,
197            }
198        }
199    }
200
201    #[inline]
202    pub const fn from_days(d: i64, scale: Scale) -> Dt {
203        Self::from_sec(d.saturating_mul(SEC_PER_DAYI64), scale)
204    }
205
206    #[inline]
207    pub const fn wk(wk: i64, scale: Scale) -> Dt {
208        Dt::from_sec(wk.saturating_mul(SEC_PER_WEEK), scale)
209    }
210
211    #[inline]
212    pub const fn yr(yr: i64, scale: Scale) -> Dt {
213        Dt::from_sec(yr.saturating_mul(31_557_600), scale)
214    }
215
216    /// Returns a `Dt` that is this duration ago from the given scale.
217    #[inline]
218    pub const fn ago(self, scale: Scale) -> Dt {
219        Dt::from(0, 0, scale).sub(self)
220    }
221
222    /// Returns a `Dt` that is this duration from now in the given scale.
223    #[inline]
224    pub const fn from_now(self, scale: Scale) -> Dt {
225        Dt::from(0, 0, scale).add(self)
226    }
227
228    /// Returns the negation of this duration.
229    #[inline]
230    pub const fn neg(self) -> Self {
231        if self.attos == 0 {
232            Self {
233                sec: -self.sec,
234                attos: 0,
235            }
236        } else {
237            Self {
238                sec: -self.sec - 1,
239                attos: ATTOS_PER_SEC - self.attos,
240            }
241        }
242    }
243
244    /// Returns the positive of this duration.
245    #[inline]
246    pub const fn abs(self) -> Self {
247        Self::from_attos(self.to_attos().saturating_abs(), Scale::TAI)
248    }
249
250    /// Creates a `Dt` from a floating-point number of seconds.
251    #[inline]
252    pub const fn from_sec_f(sec_f: Real) -> Self {
253        Self::from_sec_f_on(sec_f, Scale::TAI)
254    }
255
256    /// High-precision conversion from f64 seconds to total attoseconds (i128).
257    /// Uses IEEE 754 bit extraction + exact integer multiplication by 5^18.
258    pub const fn sec_f_to_total_attos(sec_f: f64) -> i128 {
259        if sec_f == 0.0 {
260            return 0;
261        }
262
263        let bits = sec_f.to_bits();
264        let is_negative = (bits >> 63) != 0;
265        let biased_exp = ((bits >> 52) & 0x7ff) as i32;
266        let mantissa = bits & 0x000f_ffff_ffff_ffff;
267
268        // Extract significand and true binary exponent
269        let (sig, exp) = if biased_exp == 0 {
270            // Subnormal
271            if mantissa == 0 {
272                return 0;
273            }
274            let lz = mantissa.leading_zeros() as i32;
275            let shift = lz - 11;
276            let sig = (mantissa as u128) << shift;
277            (sig, -1022i32 - 52 + shift)
278        } else {
279            let sig = ((1u64 << 52) | mantissa) as u128;
280            (sig, biased_exp - 1023 - 52)
281        };
282
283        const FIVE_POW_18: u64 = 3_814_697_265_625; // 5^18 exactly
284        let product = sig * (FIVE_POW_18 as u128);
285        let total_exp = exp + 18;
286
287        // Saturation guard for extremely large values
288        if total_exp > 120 {
289            return if is_negative { i128::MIN } else { i128::MAX };
290        }
291        if total_exp < -200 {
292            return 0;
293        }
294
295        let abs_total = if total_exp >= 0 {
296            if total_exp >= 128 {
297                return if is_negative { i128::MIN } else { i128::MAX };
298            }
299            (product << total_exp) as i128
300        } else {
301            let shift = (-total_exp) as u32;
302            let int_part = (product >> shift) as i128;
303
304            // === ROUNDING CHOICE ===
305            // Round to nearest, half away from zero.
306            // This is recommended for maximum precision.
307            // (Better average error than truncation.)
308            let mask = (1u128 << shift) - 1;
309            let rem = product & mask;
310            if rem > (mask >> 1) {
311                int_part + 1
312            } else {
313                int_part
314            }
315        };
316
317        if is_negative { -abs_total } else { abs_total }
318    }
319
320    /// Creates a `Dt` from a floating-point number of seconds.
321    /// - Assumes the value is on the given scale.
322    /// - Converts the values **to TAI**, the returned [`Dt`] is on
323    ///   the TAI time scale.
324    pub const fn from_sec_f_on(sec_f: Real, s: Scale) -> Dt {
325        if sec_f.is_nan() {
326            return Self::ZERO;
327        } else if sec_f.is_infinite() {
328            return if sec_f.is_sign_positive() {
329                Self::MAX
330            } else {
331                Self::MIN
332            };
333        }
334
335        let total_attos = Self::sec_f_to_total_attos(sec_f);
336
337        // Split into floor(seconds) and attos in [0, ATTOS_PER_SEC)
338        // Using div_euclid / rem_euclid guarantees a non-negative remainder
339        // for both positive and negative total_attos.
340        let floor_sec = total_attos.div_euclid(ATTOS_PER_SEC_I128);
341        let mut attos = total_attos.rem_euclid(ATTOS_PER_SEC_I128);
342
343        // Noise suppression? treat sub-attosecond values as exactly zero.
344        // This prevents floating-point noise (from the f64 input) from
345        // turning clean integer seconds into non-zero attos.
346        if attos.abs() < 1 {
347            attos = 0;
348        }
349
350        let total = floor_sec * ATTOS_PER_SEC_I128 + attos;
351        Self::from_attos(total, s)
352    }
353
354    /// Returns the current system time as TAI from 2000-01-01 noon.
355    ///
356    /// This method is only available when the `std` feature is enabled and the target
357    /// is not WASM with the `js` feature.
358    #[cfg(all(feature = "std", not(all(target_arch = "wasm32", feature = "js"))))]
359    #[inline]
360    pub fn now() -> Self {
361        let now = std::time::SystemTime::now();
362        let (secs, nanos) = match now.duration_since(std::time::UNIX_EPOCH) {
363            Ok(dur) => (dur.as_secs() as i64, dur.subsec_nanos() as i64),
364            Err(_) => {
365                // System time is before Unix epoch — support negative time
366                let dur = std::time::SystemTime::UNIX_EPOCH
367                    .duration_since(now)
368                    .unwrap();
369                (-(dur.as_secs() as i64), -(dur.subsec_nanos() as i64))
370            }
371        };
372
373        Dt::from_diff_and_scale(Dt::new(secs, 0), Dt::UNIX_EPOCH, Scale::UTC)
374            .add(Dt::from_ns(nanos as i128, Scale::TAI))
375    }
376
377    /// Returns the current system time as TAI from 2000-01-01 noon.
378    /// (browser WASM version using JavaScript’s `Date.now()`).
379    #[cfg(all(target_arch = "wasm32", feature = "js"))]
380    #[inline]
381    pub fn now() -> Self {
382        let ms: f64 = js_sys::Date::now();
383        let secs = (ms / 1000.0).floor() as i64;
384        let nanos = ((ms % 1000.0) * 1_000_000.0) as i128;
385
386        Dt::from_diff_and_scale(Dt::new(secs, 0), Dt::UNIX_EPOCH, Scale::UTC)
387            .add(Dt::from_ns(nanos as i128, Scale::TAI))
388    }
389}