Skip to main content

deep_time/dt/
constructors.rs

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