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