Coverage Report

Created: 2026-05-16 06:09

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/hifitime-4.3.0/src/timeseries.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 super::{Duration, Epoch};
12
13
use core::fmt;
14
15
#[cfg(not(feature = "std"))]
16
#[allow(unused_imports)] // Import is indeed used.
17
use num_traits::Float;
18
19
#[cfg(feature = "python")]
20
use pyo3::prelude::*;
21
22
/*
23
24
NOTE: This is taken from itertools: https://docs.rs/itertools-num/0.1.3/src/itertools_num/linspace.rs.html#78-93 .
25
26
*/
27
28
/// An iterator of a sequence of evenly spaced Epochs.
29
///
30
/// (Python documentation hints)
31
/// :type start: Epoch
32
/// :type end: Epoch
33
/// :type step: Duration
34
/// :type inclusive: bool
35
#[cfg_attr(kani, derive(kani::Arbitrary))]
36
#[derive(Clone, Debug, PartialEq, Eq)]
37
#[cfg_attr(feature = "python", pyclass)]
38
#[cfg_attr(feature = "python", pyo3(module = "hifitime"))]
39
pub struct TimeSeries {
40
    start: Epoch,
41
    duration: Duration,
42
    step: Duration,
43
    cur: i64,
44
    incl: bool,
45
}
46
47
impl TimeSeries {
48
    /// Return an iterator of evenly spaced Epochs, **inclusive** on start and **exclusive** on end.
49
    /// ```
50
    /// use hifitime::{Epoch, Unit, TimeSeries};
51
    /// let start = Epoch::from_gregorian_utc_at_midnight(2017, 1, 14);
52
    /// let end = Epoch::from_gregorian_utc_at_noon(2017, 1, 14);
53
    /// let step = Unit::Hour * 2;
54
    /// let time_series = TimeSeries::exclusive(start, end, step);
55
    /// let mut cnt = 0;
56
    /// for epoch in time_series {
57
    ///     println!("{}", epoch);
58
    ///     cnt += 1
59
    /// }
60
    /// assert_eq!(cnt, 6)
61
    /// ```
62
    #[inline]
63
0
    pub fn exclusive(start: Epoch, end: Epoch, step: Duration) -> TimeSeries {
64
        // Start one step prior to start because next() just moves forward
65
0
        Self {
66
0
            start,
67
0
            duration: end - start,
68
0
            step,
69
0
            cur: 0,
70
0
            incl: false,
71
0
        }
72
0
    }
73
74
    /// Returns first [Epoch] of this [TimeSeries], without consuming
75
    /// the iterator.
76
    #[inline]
77
0
    pub fn first_epoch(&self) -> Epoch {
78
0
        self.start
79
0
    }
80
81
    /// Returns last [Epoch] of this [TimeSeries], without consuming
82
    /// the iterator.
83
    #[inline]
84
0
    pub fn last_epoch(&self) -> Epoch {
85
0
        let mut epoch = self.start + self.duration;
86
0
        if !self.incl {
87
0
            // remove one step
88
0
            epoch -= self.step;
89
0
        }
90
0
        epoch
91
0
    }
92
93
    /// Return an iterator of evenly spaced Epochs, inclusive on start **and** on end.
94
    /// ```
95
    /// use hifitime::{Epoch, Unit, TimeSeries};
96
    /// let start = Epoch::from_gregorian_utc_at_midnight(2017, 1, 14);
97
    /// let end = Epoch::from_gregorian_utc_at_noon(2017, 1, 14);
98
    /// let step = Unit::Hour * 2;
99
    /// let time_series = TimeSeries::inclusive(start, end, step);
100
    /// let mut cnt = 0;
101
    /// for epoch in time_series {
102
    ///     println!("{}", epoch);
103
    ///     cnt += 1
104
    /// }
105
    /// assert_eq!(cnt, 7)
106
    /// ```
107
    #[inline]
108
0
    pub fn inclusive(start: Epoch, end: Epoch, step: Duration) -> TimeSeries {
109
        // Start one step prior to start because next() just moves forward
110
0
        Self {
111
0
            start,
112
0
            duration: end - start,
113
0
            step,
114
0
            cur: 0,
115
0
            incl: true,
116
0
        }
117
0
    }
118
}
119
120
impl fmt::Display for TimeSeries {
121
    // Prints this duration with automatic selection of the units, i.e. everything that isn't zero is ignored
122
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
123
0
        write!(
124
0
            f,
125
0
            "TimeSeries [{} : {} : {}]",
126
            self.start,
127
0
            if self.incl {
128
0
                self.start + self.duration
129
            } else {
130
0
                self.start + self.duration - self.step
131
            },
132
            self.step
133
        )
134
0
    }
135
}
136
137
impl fmt::LowerHex for TimeSeries {
138
    /// Prints the Epoch in TAI
139
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
140
0
        write!(
141
0
            f,
142
0
            "TimeSeries [{:x} : {:x} : {}]",
143
            self.start,
144
0
            if self.incl {
145
0
                self.start + self.duration
146
            } else {
147
0
                self.start + self.duration - self.step
148
            },
149
            self.step
150
        )
151
0
    }
152
}
153
154
impl fmt::UpperHex for TimeSeries {
155
    /// Prints the Epoch in TT
156
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
157
0
        write!(
158
0
            f,
159
0
            "TimeSeries [{:X} : {:X} : {}]",
160
            self.start,
161
0
            if self.incl {
162
0
                self.start + self.duration
163
            } else {
164
0
                self.start + self.duration - self.step
165
            },
166
            self.step
167
        )
168
0
    }
169
}
170
171
impl fmt::LowerExp for TimeSeries {
172
    /// Prints the Epoch in TDB
173
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
174
0
        write!(
175
0
            f,
176
0
            "TimeSeries [{:e} : {:e} : {}]",
177
            self.start,
178
0
            if self.incl {
179
0
                self.start + self.duration
180
            } else {
181
0
                self.start + self.duration - self.step
182
            },
183
            self.step
184
        )
185
0
    }
186
}
187
188
impl fmt::UpperExp for TimeSeries {
189
    /// Prints the Epoch in ET
190
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
191
0
        write!(
192
0
            f,
193
0
            "TimeSeries [{:E} : {:E} : {}]",
194
            self.start,
195
0
            if self.incl {
196
0
                self.start + self.duration
197
            } else {
198
0
                self.start + self.duration - self.step
199
            },
200
            self.step
201
        )
202
0
    }
203
}
204
205
impl fmt::Pointer for TimeSeries {
206
    /// Prints the Epoch in UNIX
207
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
208
0
        write!(
209
0
            f,
210
0
            "TimeSeries [{:p} : {:p} : {}]",
211
            self.start,
212
0
            if self.incl {
213
0
                self.start + self.duration
214
            } else {
215
0
                self.start + self.duration - self.step
216
            },
217
            self.step
218
        )
219
0
    }
220
}
221
222
impl fmt::Octal for TimeSeries {
223
    /// Prints the Epoch in GPS
224
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
225
0
        write!(
226
0
            f,
227
0
            "TimeSeries [{:o} : {:o} : {}]",
228
            self.start,
229
0
            if self.incl {
230
0
                self.start + self.duration
231
            } else {
232
0
                self.start + self.duration - self.step
233
            },
234
            self.step
235
        )
236
0
    }
237
}
238
239
#[cfg(feature = "python")]
240
#[pymethods]
241
impl TimeSeries {
242
    #[new]
243
    /// Return an iterator of evenly spaced Epochs
244
    /// If inclusive is set to true, this iterator is inclusive on start **and** on end.
245
    /// If inclusive is set to false, only the start epoch is included in the iteration.
246
    fn new_py(start: Epoch, end: Epoch, step: Duration, inclusive: bool) -> Self {
247
        if inclusive {
248
            Self::inclusive(start, end, step)
249
        } else {
250
            Self::exclusive(start, end, step)
251
        }
252
    }
253
254
    fn __getnewargs__(&self) -> Result<(Epoch, Epoch, Duration, bool), PyErr> {
255
        Ok((self.start, self.start + self.duration, self.step, self.incl))
256
    }
257
258
    fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
259
        slf
260
    }
261
262
    fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<Epoch> {
263
        slf.next()
264
    }
265
266
    fn __str__(&self) -> String {
267
        format!("{self}")
268
    }
269
270
    fn __repr__(&self) -> String {
271
        format!("{self:?} @ {self:p}")
272
    }
273
274
    #[cfg(feature = "python")]
275
    fn __eq__(&self, other: Self) -> bool {
276
        *self == other
277
    }
278
}
279
280
impl Iterator for TimeSeries {
281
    type Item = Epoch;
282
283
    #[inline]
284
0
    fn next(&mut self) -> Option<Epoch> {
285
0
        let next_offset = self.cur * self.step;
286
0
        if (!self.incl && next_offset >= self.duration)
287
0
            || (self.incl && next_offset > self.duration)
288
        {
289
0
            None
290
        } else {
291
0
            self.cur += 1;
292
0
            Some(self.start + next_offset)
293
        }
294
0
    }
Unexecuted instantiation: <hifitime::timeseries::TimeSeries as core::iter::traits::iterator::Iterator>::next
Unexecuted instantiation: <hifitime::timeseries::TimeSeries as core::iter::traits::iterator::Iterator>::next
295
296
0
    fn size_hint(&self) -> (usize, Option<usize>) {
297
0
        (self.len(), Some(self.len() + 1))
298
0
    }
299
}
300
301
impl DoubleEndedIterator for TimeSeries {
302
    #[inline]
303
0
    fn next_back(&mut self) -> Option<Epoch> {
304
        // Offset from the end of the iterator
305
0
        self.cur += 1;
306
0
        let offset = self.cur * self.step;
307
        // if offset < -self.duration - self.step {
308
0
        if (!self.incl && offset > self.duration)
309
0
            || (self.incl && offset > self.duration + self.step)
310
        {
311
0
            None
312
        } else {
313
0
            Some(self.start + self.duration - offset)
314
        }
315
0
    }
316
}
317
318
impl ExactSizeIterator for TimeSeries
319
where
320
    TimeSeries: Iterator,
321
{
322
0
    fn len(&self) -> usize {
323
0
        let approx = (self.duration.to_seconds() / self.step.to_seconds()).abs();
324
0
        if self.incl {
325
0
            if approx.ceil() >= usize::MAX as f64 {
326
0
                usize::MAX
327
            } else {
328
0
                approx.ceil() as usize
329
            }
330
0
        } else if approx.floor() >= usize::MAX as f64 {
331
0
            usize::MAX
332
        } else {
333
0
            approx.floor() as usize
334
        }
335
0
    }
336
}
337
338
#[cfg(test)]
339
mod tests {
340
    use crate::{Epoch, TimeSeries, Unit};
341
342
    #[test]
343
    fn test_exclusive_timeseries() {
344
        let start = Epoch::from_gregorian_utc_at_midnight(2017, 1, 14);
345
        let end = Epoch::from_gregorian_utc_at_noon(2017, 1, 14);
346
        let step = Unit::Hour * 2;
347
348
        let mut count = 0;
349
        let time_series = TimeSeries::exclusive(start, end, step);
350
351
        assert_eq!(time_series.first_epoch(), start, "invalid first epoch");
352
        assert_eq!(time_series.last_epoch(), end - step, "invalid last epoch");
353
354
        for epoch in time_series {
355
            if count == 0 {
356
                assert_eq!(
357
                    epoch, start,
358
                    "Starting epoch of exclusive time series is wrong"
359
                );
360
            } else if count == 5 {
361
                assert_ne!(epoch, end, "Ending epoch of exclusive time series is wrong");
362
            }
363
            #[cfg(feature = "std")]
364
            println!("tests::exclusive_timeseries::{epoch}");
365
            count += 1;
366
        }
367
368
        assert_eq!(count, 6, "Should have five items in this iterator");
369
    }
370
371
    #[test]
372
    fn test_inclusive_timeseries() {
373
        let start = Epoch::from_gregorian_utc_at_midnight(2017, 1, 14);
374
        let end = Epoch::from_gregorian_utc_at_noon(2017, 1, 14);
375
        let step = Unit::Hour * 2;
376
377
        let mut count = 0;
378
        let time_series = TimeSeries::inclusive(start, end, step);
379
380
        assert_eq!(time_series.first_epoch(), start, "invalid first epoch");
381
        assert_eq!(time_series.last_epoch(), end, "invalid last epoch");
382
383
        for epoch in time_series {
384
            if count == 0 {
385
                assert_eq!(
386
                    epoch, start,
387
                    "Starting epoch of inclusive time series is wrong"
388
                );
389
            } else if count == 6 {
390
                assert_eq!(epoch, end, "Ending epoch of inclusive time series is wrong");
391
            }
392
            #[cfg(feature = "std")]
393
            println!("tests::inclusive_timeseries::{epoch}");
394
            count += 1;
395
        }
396
397
        assert_eq!(count, 7, "Should have six items in this iterator");
398
    }
399
400
    #[test]
401
    fn gh131_regression() {
402
        let start = Epoch::from_gregorian_utc(2022, 7, 14, 2, 56, 11, 228271007);
403
        let step = 0.5 * Unit::Microsecond;
404
        let steps = 1_000_000_000;
405
        let end = start + steps * step; // This is 500 ms later
406
        let times = TimeSeries::exclusive(start, end, step);
407
        // For an _exclusive_ time series, we skip the last item, so it's steps minus one
408
        assert_eq!(times.len(), steps as usize - 1);
409
        assert_eq!(times.len(), times.size_hint().0);
410
411
        // For an _inclusive_ time series, we skip the last item, so it's the steps count
412
        let times = TimeSeries::inclusive(start, end, step);
413
        assert_eq!(times.len(), steps as usize);
414
        assert_eq!(times.len(), times.size_hint().0);
415
    }
416
417
    #[test]
418
    fn ts_over_leap_second() {
419
        let start = Epoch::from_gregorian_utc(2016, 12, 31, 23, 59, 59, 0);
420
        let end = start + Unit::Second * 5;
421
        let step = Unit::Second * 1;
422
423
        let times = TimeSeries::exclusive(start, end, step);
424
        let expect_end = start + Unit::Second * 4;
425
        let mut cnt = 0;
426
        let mut cur_epoch = start;
427
428
        assert_eq!(times.first_epoch(), start, "invalid first epoch");
429
        assert_eq!(times.last_epoch(), end - step, "invalid last epoch");
430
431
        for epoch in times {
432
            cnt += 1;
433
            cur_epoch = epoch;
434
        }
435
436
        assert_eq!(cnt, 5); // Five because the first item is always inclusive
437
        assert_eq!(cur_epoch, expect_end, "incorrect last item in iterator");
438
    }
439
440
    #[test]
441
    fn ts_backward() {
442
        let start = Epoch::from_gregorian_utc(2015, 1, 1, 12, 0, 0, 0);
443
        let end = start + Unit::Second * 5;
444
        let step = Unit::Second * 1;
445
        let times = TimeSeries::exclusive(start, end, step);
446
        let mut cnt = 0;
447
        let mut cur_epoch = start;
448
449
        assert_eq!(times.first_epoch(), start, "invalid first epoch");
450
        assert_eq!(times.last_epoch(), end - step, "invalid last epoch");
451
452
        for epoch in times.rev() {
453
            cnt += 1;
454
            cur_epoch = epoch;
455
            let expect = start + Unit::Second * (5 - cnt);
456
            assert_eq!(expect, epoch, "incorrect item in iterator");
457
        }
458
459
        assert_eq!(cnt, 5); // Five because the first item is always inclusive
460
        assert_eq!(cur_epoch, start, "incorrect last item in iterator");
461
    }
462
}