Coverage Report

Created: 2026-09-14 07:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/hifitime-4.3.1/src/epoch/ops.rs
Line
Count
Source
1
/*
2
* Hifitime
3
* Copyright (C) 2017-onward Christopher Rabotin <christopher.rabotin@gmail.com> et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors)
4
* This Source Code Form is subject to the terms of the Mozilla Public
5
* License, v. 2.0. If a copy of the MPL was not distributed with this
6
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
*
8
* Documentation: https://nyxspace.com/
9
*/
10
11
use core::cmp::{Ord, Ordering, PartialEq, PartialOrd};
12
use core::hash::{Hash, Hasher};
13
use core::ops::{Add, AddAssign, Sub, SubAssign};
14
15
use crate::{
16
    errors::HifitimeError, Duration, Epoch, Polynomial, TimeScale, Unit, Weekday,
17
    NANOSECONDS_PER_DAY,
18
};
19
20
#[cfg(feature = "python")]
21
use pyo3::prelude::*;
22
23
#[cfg(not(feature = "std"))]
24
#[allow(unused_imports)] // Import is indeed used.
25
use num_traits::Float;
26
27
use super::rem_euclid_f64;
28
29
#[cfg_attr(feature = "python", pymethods)]
30
impl Epoch {
31
    /// Returns the minimum of the two epochs.
32
    ///
33
    /// ```
34
    /// use hifitime::Epoch;
35
    ///
36
    /// let e0 = Epoch::from_gregorian_utc_at_midnight(2022, 10, 20);
37
    /// let e1 = Epoch::from_gregorian_utc_at_midnight(2022, 10, 21);
38
    ///
39
    /// assert_eq!(e0, e1.min(e0));
40
    /// assert_eq!(e0, e0.min(e1));
41
    /// ```
42
    ///
43
    /// _Note:_ this uses a pointer to `self` which will be copied immediately because Python requires a pointer.
44
    /// :type other: Epoch
45
    /// :rtype: Epoch
46
0
    pub fn min(&self, other: Self) -> Self {
47
0
        if *self < other {
48
0
            *self
49
        } else {
50
0
            other
51
        }
52
0
    }
53
54
    /// Returns the maximum of the two epochs.
55
    ///
56
    /// ```
57
    /// use hifitime::Epoch;
58
    ///
59
    /// let e0 = Epoch::from_gregorian_utc_at_midnight(2022, 10, 20);
60
    /// let e1 = Epoch::from_gregorian_utc_at_midnight(2022, 10, 21);
61
    ///
62
    /// assert_eq!(e1, e1.max(e0));
63
    /// assert_eq!(e1, e0.max(e1));
64
    /// ```
65
    ///
66
    /// _Note:_ this uses a pointer to `self` which will be copied immediately because Python requires a pointer.
67
    /// :type other: Epoch
68
    /// :rtype: Epoch
69
0
    pub fn max(&self, other: Self) -> Self {
70
0
        if *self > other {
71
0
            *self
72
        } else {
73
0
            other
74
        }
75
0
    }
76
77
    #[must_use]
78
    /// Floors this epoch to the closest provided duration
79
    ///
80
    /// # Example
81
    /// ```
82
    /// use hifitime::{Epoch, TimeUnits};
83
    ///
84
    /// let e = Epoch::from_gregorian_tai_hms(2022, 5, 20, 17, 57, 43);
85
    /// assert_eq!(
86
    ///     e.floor(1.hours()),
87
    ///     Epoch::from_gregorian_tai_hms(2022, 5, 20, 17, 0, 0)
88
    /// );
89
    ///
90
    /// let e = Epoch::from_gregorian_tai(2022, 10, 3, 17, 44, 29, 898032665);
91
    /// assert_eq!(
92
    ///     e.floor(3.minutes()),
93
    ///     Epoch::from_gregorian_tai_hms(2022, 10, 3, 17, 42, 0)
94
    /// );
95
    /// ```
96
    /// :type duration: Duration
97
    /// :rtype: Epoch
98
0
    pub fn floor(&self, duration: Duration) -> Self {
99
0
        Self::from_duration(self.duration.floor(duration), self.time_scale)
100
0
    }
101
102
    #[must_use]
103
    /// Ceils this epoch to the closest provided duration in the TAI time scale
104
    ///
105
    /// # Example
106
    /// ```
107
    /// use hifitime::{Epoch, TimeUnits};
108
    ///
109
    /// let e = Epoch::from_gregorian_tai_hms(2022, 5, 20, 17, 57, 43);
110
    /// assert_eq!(
111
    ///     e.ceil(1.hours()),
112
    ///     Epoch::from_gregorian_tai_hms(2022, 5, 20, 18, 0, 0)
113
    /// );
114
    ///
115
    /// // 45 minutes is a multiple of 3 minutes, hence this result
116
    /// let e = Epoch::from_gregorian_tai(2022, 10, 3, 17, 44, 29, 898032665);
117
    /// assert_eq!(
118
    ///     e.ceil(3.minutes()),
119
    ///     Epoch::from_gregorian_tai_hms(2022, 10, 3, 17, 45, 0)
120
    /// );
121
    /// ```
122
    /// :type duration: Duration
123
    /// :rtype: Epoch
124
0
    pub fn ceil(&self, duration: Duration) -> Self {
125
0
        Self::from_duration(self.duration.ceil(duration), self.time_scale)
126
0
    }
127
128
    #[must_use]
129
    /// Rounds this epoch to the closest provided duration in TAI
130
    ///
131
    /// # Example
132
    /// ```
133
    /// use hifitime::{Epoch, TimeUnits};
134
    ///
135
    /// let e = Epoch::from_gregorian_tai_hms(2022, 5, 20, 17, 57, 43);
136
    /// assert_eq!(
137
    ///     e.round(1.hours()),
138
    ///     Epoch::from_gregorian_tai_hms(2022, 5, 20, 18, 0, 0)
139
    /// );
140
    /// ```
141
    /// :type duration: Duration
142
    /// :rtype: Epoch
143
0
    pub fn round(&self, duration: Duration) -> Self {
144
0
        Self::from_duration(self.duration.round(duration), self.time_scale)
145
0
    }
146
147
    #[must_use]
148
    /// Converts this epoch into the time of week, represented as a rolling week counter into that time scale
149
    /// and the number of nanoseconds elapsed in current week (since closest Sunday midnight).
150
    /// This is usually how GNSS receivers describe a timestamp.
151
    /// :rtype: tuple
152
0
    pub fn to_time_of_week(&self) -> (u32, u64) {
153
0
        let total_nanoseconds = self.duration.total_nanoseconds();
154
0
        let weeks = total_nanoseconds / NANOSECONDS_PER_DAY as i128 / Weekday::DAYS_PER_WEEK_I128;
155
        // elapsed nanoseconds in current week:
156
        //   remove previously determined nb of weeks
157
        //   get residual nanoseconds
158
0
        let nanoseconds =
159
0
            total_nanoseconds - weeks * NANOSECONDS_PER_DAY as i128 * Weekday::DAYS_PER_WEEK_I128;
160
0
        (weeks as u32, nanoseconds as u64)
161
0
    }
162
163
    /// Converts this [Epoch] into targeted [TimeScale] using provided [Polynomial].
164
    ///
165
    /// ## Input
166
    /// - forward: whether this is forward or backward conversion.
167
    ///   For example, using GPST-UTC [Polynomial]
168
    ///   - GPST->UTC is the forward conversion
169
    ///   - UTC->GPST is the backward conversion
170
    /// - reference_epoch: any reference [Epoch] for the provided [Polynomial].  
171
    ///
172
    /// While we support any time difference, it should remain short in pratice (a day at most, for precise applications).
173
    /// - polynomial: that must be valid for this reference [Epoch], used in the equation `a0 + a1*dt + a2*dt² = GPST-UTC` for example.
174
    /// - target: targetted [TimeScale] we will transition to.
175
    ///
176
    /// Example:
177
    /// ```
178
    /// use hifitime::{Epoch, TimeScale, Polynomial, Unit};
179
    ///
180
    /// // random GPST Epoch for forward conversion to UTC
181
    /// let t_gpst = Epoch::from_gregorian(2020, 01, 01, 0, 0, 0, 0, TimeScale::GPST);
182
    ///
183
    /// // Let's say we know the GPST-UTC polynomials for that day,
184
    /// // They allow precise forward transition from GPST to UTC,
185
    /// // and precise backward transition from UTC to GPST.
186
    /// let gpst_utc_polynomials = Polynomial::from_constant_offset_nanoseconds(1.0);
187
    ///
188
    /// // This is the reference [Epoch] attached to the publication of these polynomials.
189
    /// // You should use polynomials that remain valid and were provided recently (usually one day at most).
190
    /// // Example: polynomials were published 1 hour ago.
191
    /// let gpst_reference = t_gpst - 1.0 * Unit::Hour;
192
    ///
193
    /// // Forward conversion (to UTC) GPST - a0 + a1 *dt + a2*dt² = UTC
194
    /// let t_utc = t_gpst.precise_timescale_conversion(true, gpst_reference, gpst_utc_polynomials, TimeScale::UTC)
195
    ///     .unwrap();
196
    ///
197
    /// // Verify we did transition to UTC
198
    /// assert_eq!(t_utc.time_scale, TimeScale::UTC);
199
    ///
200
    /// // Verify the resulting [Epoch] is the coarse GPST->UTC transition + fine correction
201
    /// let reversed = t_utc.to_time_scale(TimeScale::GPST) + 1.0 * Unit::Nanosecond;
202
    /// assert_eq!(reversed, t_gpst);
203
    ///
204
    /// // Apply the backward transition, from t_utc back to t_gpst.
205
    /// // The timescale conversion works both ways: (from UTC) GPST = UTC + a0 + a1 *dt + a2*dt²
206
    /// let backwards = t_utc.precise_timescale_conversion(false, gpst_reference, gpst_utc_polynomials, TimeScale::GPST)
207
    ///     .unwrap();
208
    ///
209
    /// assert_eq!(backwards, t_gpst);
210
    ///
211
    /// // It is important to understand that your reference point does not have to be in the past.
212
    /// // The only logic that should prevail is to always minimize interpolation gap.
213
    /// // In other words, if you can access future interpolation information that would minimize the data gap, they should prevail.
214
    /// // Example: +30' in the future.
215
    /// let gpst_reference = t_gpst + 30.0 * Unit::Minute;
216
    ///
217
    /// // Forward conversion (to UTC) but using polynomials that were released 1 hour after t_gpst
218
    /// let t_utc = t_gpst.precise_timescale_conversion(true, gpst_reference, gpst_utc_polynomials, TimeScale::UTC)
219
    ///     .unwrap();
220
    ///
221
    /// // Verifications
222
    /// assert_eq!(t_utc.time_scale, TimeScale::UTC);
223
    ///
224
    /// let reversed = t_utc.to_time_scale(TimeScale::GPST) + 1.0 * Unit::Nanosecond;
225
    /// assert_eq!(reversed, t_gpst);
226
    /// ```
227
    /// :type forward: bool
228
    /// :type reference_epoch: Epoch
229
    /// :type polynomial: Polynomial
230
    /// :type target: TimeScale
231
    /// :rtype: Epoch
232
0
    pub fn precise_timescale_conversion(
233
0
        &self,
234
0
        forward: bool,
235
0
        reference_epoch: Self,
236
0
        polynomial: Polynomial,
237
0
        target: TimeScale,
238
0
    ) -> Result<Self, HifitimeError> {
239
0
        if self.time_scale == target {
240
            // Incorrect operation.
241
0
            return Err(HifitimeError::SystemTimeError);
242
0
        }
243
244
0
        let reference_epoch = reference_epoch.to_time_scale(self.time_scale);
245
246
        // supports any interpolation gap. But applications should remain within
247
        // current week (to the very least..)
248
0
        let dt = *self - reference_epoch;
249
0
        let correction = polynomial.correction_duration(dt);
250
251
        // coarse conversion
252
0
        let converted = self.to_time_scale(target);
253
254
        // fine correction
255
0
        if forward {
256
            // GPST-UTC = a0+a1+a2
257
            //      UTC = GPST -a0-a1-a2
258
0
            Ok(converted - correction)
259
        } else {
260
            // GPST-UTC = a0+a1+a2
261
            // GPST     = a0+a1+a2 + UTC
262
0
            Ok(converted + correction)
263
        }
264
0
    }
265
266
    #[must_use]
267
    /// Returns the weekday in provided time scale **ASSUMING** that the reference epoch of that time scale is a Monday.
268
    /// You _probably_ do not want to use this. You probably either want `weekday()` or `weekday_utc()`.
269
    /// Several time scales do _not_ have a reference day that's on a Monday, e.g. BDT.
270
    /// :type time_scale: TimeScale
271
    /// :rtype: Weekday
272
0
    pub fn weekday_in_time_scale(&self, time_scale: TimeScale) -> Weekday {
273
0
        (rem_euclid_f64(
274
0
            self.to_duration_in_time_scale(time_scale)
275
0
                .to_unit(Unit::Day),
276
0
            Weekday::DAYS_PER_WEEK,
277
0
        )
278
0
        .floor() as u8)
279
0
            .into()
280
0
    }
281
282
    #[must_use]
283
    /// Returns weekday (uses the TAI representation for this calculation).
284
    /// :rtype: Weekday
285
    #[cfg_attr(kani, kani::ensures(|result: &Weekday| (*result as u8) < 7))]
286
0
    pub fn weekday(&self) -> Weekday {
287
        // J1900 was a Monday so we just have to modulo the number of days by the number of days per week.
288
        // The function call will be optimized away.
289
0
        self.weekday_in_time_scale(TimeScale::TAI)
290
0
    }
291
292
    #[must_use]
293
    /// Returns weekday in UTC timescale
294
    /// :rtype: Weekday
295
0
    pub fn weekday_utc(&self) -> Weekday {
296
0
        self.weekday_in_time_scale(TimeScale::UTC)
297
0
    }
298
299
    #[must_use]
300
    /// Returns the next weekday.
301
    ///
302
    /// ```
303
    /// use hifitime::prelude::*;
304
    ///
305
    /// let epoch = Epoch::from_gregorian_utc_at_midnight(1988, 1, 2);
306
    /// assert_eq!(epoch.weekday_utc(), Weekday::Saturday);
307
    /// assert_eq!(epoch.next(Weekday::Sunday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 3));
308
    /// assert_eq!(epoch.next(Weekday::Monday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 4));
309
    /// assert_eq!(epoch.next(Weekday::Tuesday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 5));
310
    /// assert_eq!(epoch.next(Weekday::Wednesday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 6));
311
    /// assert_eq!(epoch.next(Weekday::Thursday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 7));
312
    /// assert_eq!(epoch.next(Weekday::Friday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 8));
313
    /// assert_eq!(epoch.next(Weekday::Saturday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 9));
314
    /// ```
315
    /// :type weekday: Weekday
316
    /// :rtype: Epoch
317
0
    pub fn next(&self, weekday: Weekday) -> Self {
318
0
        let delta_days = self.weekday() - weekday;
319
0
        if delta_days == Duration::ZERO {
320
0
            *self + 7 * Unit::Day
321
        } else {
322
0
            *self + delta_days
323
        }
324
0
    }
325
326
    /// :type weekday: Weekday
327
    /// :rtype: Epoch
328
    #[must_use]
329
0
    pub fn next_weekday_at_midnight(&self, weekday: Weekday) -> Self {
330
0
        self.next(weekday).with_hms_strict(0, 0, 0)
331
0
    }
332
333
    /// :type weekday: Weekday
334
    /// :rtype: Epoch
335
    #[must_use]
336
0
    pub fn next_weekday_at_noon(&self, weekday: Weekday) -> Self {
337
0
        self.next(weekday).with_hms_strict(12, 0, 0)
338
0
    }
339
340
    #[must_use]
341
    /// Returns the next weekday.
342
    ///
343
    /// ```
344
    /// use hifitime::prelude::*;
345
    ///
346
    /// let epoch = Epoch::from_gregorian_utc_at_midnight(1988, 1, 2);
347
    /// assert_eq!(epoch.previous(Weekday::Friday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 1));
348
    /// assert_eq!(epoch.previous(Weekday::Thursday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 31));
349
    /// assert_eq!(epoch.previous(Weekday::Wednesday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 30));
350
    /// assert_eq!(epoch.previous(Weekday::Tuesday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 29));
351
    /// assert_eq!(epoch.previous(Weekday::Monday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 28));
352
    /// assert_eq!(epoch.previous(Weekday::Sunday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 27));
353
    /// assert_eq!(epoch.previous(Weekday::Saturday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 26));
354
    /// ```
355
    /// :type weekday: Weekday
356
    /// :rtype: Epoch
357
0
    pub fn previous(&self, weekday: Weekday) -> Self {
358
0
        let delta_days = weekday - self.weekday();
359
0
        if delta_days == Duration::ZERO {
360
0
            *self - 7 * Unit::Day
361
        } else {
362
0
            *self - delta_days
363
        }
364
0
    }
365
366
    /// :type weekday: Weekday
367
    /// :rtype: Epoch
368
    #[must_use]
369
0
    pub fn previous_weekday_at_midnight(&self, weekday: Weekday) -> Self {
370
0
        self.previous(weekday).with_hms_strict(0, 0, 0)
371
0
    }
372
373
    /// :type weekday: Weekday
374
    /// :rtype: Epoch
375
    #[must_use]
376
0
    pub fn previous_weekday_at_noon(&self, weekday: Weekday) -> Self {
377
0
        self.previous(weekday).with_hms_strict(12, 0, 0)
378
0
    }
379
}
380
381
impl Sub for Epoch {
382
    type Output = Duration;
383
384
28.4k
    fn sub(self, other: Self) -> Duration {
385
28.4k
        self.duration - other.to_time_scale(self.time_scale).duration
386
28.4k
    }
387
}
388
389
impl SubAssign<Duration> for Epoch {
390
0
    fn sub_assign(&mut self, duration: Duration) {
391
0
        *self = *self - duration;
392
0
    }
393
}
394
395
impl Sub<Duration> for Epoch {
396
    type Output = Self;
397
398
68.7k
    fn sub(self, duration: Duration) -> Self {
399
68.7k
        Self {
400
68.7k
            duration: self.duration - duration,
401
68.7k
            time_scale: self.time_scale,
402
68.7k
        }
403
68.7k
    }
404
}
405
406
/// WARNING: For speed, there is a possibility to add seconds directly to an Epoch. These will be added in the time scale the Epoch was initialized in.
407
/// Using this is _discouraged_ and should only be used if you have facing bottlenecks with the units.
408
impl Add<f64> for Epoch {
409
    type Output = Self;
410
411
0
    fn add(self, seconds: f64) -> Self {
412
0
        Self {
413
0
            duration: self.duration + seconds * Unit::Second,
414
0
            time_scale: self.time_scale,
415
0
        }
416
0
    }
417
}
418
419
impl Add<Duration> for Epoch {
420
    type Output = Self;
421
422
72.9k
    fn add(self, duration: Duration) -> Self {
423
72.9k
        Self {
424
72.9k
            duration: self.duration + duration,
425
72.9k
            time_scale: self.time_scale,
426
72.9k
        }
427
72.9k
    }
428
}
429
430
impl AddAssign<Unit> for Epoch {
431
    #[allow(clippy::identity_op)]
432
0
    fn add_assign(&mut self, unit: Unit) {
433
0
        *self = *self + unit * 1;
434
0
    }
435
}
436
437
impl SubAssign<Unit> for Epoch {
438
    #[allow(clippy::identity_op)]
439
0
    fn sub_assign(&mut self, unit: Unit) {
440
0
        *self = *self - unit * 1;
441
0
    }
442
}
443
444
impl Sub<Unit> for Epoch {
445
    type Output = Self;
446
447
    #[allow(clippy::identity_op)]
448
0
    fn sub(self, unit: Unit) -> Self {
449
0
        Self {
450
0
            duration: self.duration - unit * 1,
451
0
            time_scale: self.time_scale,
452
0
        }
453
0
    }
454
}
455
456
impl Add<Unit> for Epoch {
457
    type Output = Self;
458
459
    #[allow(clippy::identity_op)]
460
0
    fn add(self, unit: Unit) -> Self {
461
0
        Self {
462
0
            duration: self.duration + unit * 1,
463
0
            time_scale: self.time_scale,
464
0
        }
465
0
    }
466
}
467
468
impl AddAssign<Duration> for Epoch {
469
0
    fn add_assign(&mut self, duration: Duration) {
470
0
        *self = *self + duration;
471
0
    }
472
}
473
474
/// Equality only checks the duration since J1900 match in TAI, because this is how all of the epochs are referenced.
475
/// Equality compares epochs as points in time.
476
/// Uses field comparison via to_parts() to bypass Duration::PartialEq's
477
/// zero-crossing special case (which treats opposite-sign durations as equal).
478
impl PartialEq for Epoch {
479
0
    fn eq(&self, other: &Self) -> bool {
480
0
        if self.time_scale == other.time_scale {
481
0
            self.duration.to_parts() == other.duration.to_parts()
482
0
        } else if self.time_scale.uses_leap_seconds() != other.time_scale.uses_leap_seconds() {
483
            // If one of the two time scales includes leap seconds,
484
            // we always convert the time scale with leap seconds into the
485
            // time scale that does NOT have leap seconds.
486
0
            if self.time_scale.uses_leap_seconds() {
487
0
                self.to_time_scale(other.time_scale).duration.to_parts()
488
0
                    == other.duration.to_parts()
489
            } else {
490
0
                self.duration.to_parts() == other.to_time_scale(self.time_scale).duration.to_parts()
491
            }
492
        } else {
493
            // Otherwise it does not matter
494
0
            self.duration.to_parts() == other.to_time_scale(self.time_scale).duration.to_parts()
495
        }
496
0
    }
497
}
498
499
impl PartialOrd for Epoch {
500
47.4k
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
501
47.4k
        Some(self.cmp(other))
502
47.4k
    }
503
}
504
505
impl Ord for Epoch {
506
48.9k
    fn cmp(&self, other: &Self) -> Ordering {
507
48.9k
        self.to_tai_duration()
508
48.9k
            .to_parts()
509
48.9k
            .cmp(&other.to_tai_duration().to_parts())
510
48.9k
    }
511
}
512
513
impl Hash for Epoch {
514
    /// Hash normalizes to TAI for consistency with PartialEq.
515
0
    fn hash<H: Hasher>(&self, state: &mut H) {
516
0
        self.to_tai_duration().hash(state);
517
0
    }
518
}