/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 | | } |