Skip to main content

thruput_xdr/
time.rs

1//! Arithmetic on `TimePoint` and `BlockHeight`. The rules are documented,
2//! with an example, in `docs/types.md`, which renders as the `types` module
3//! documentation; this module is private.
4
5use core::ops::{Add, AddAssign, Sub, SubAssign};
6use core::time::Duration;
7
8use crate::types::{BlockHeight, TimePoint};
9
10/// The nanoseconds in `duration`, or `None` if they do not fit `u64` (about
11/// 584 years).
12const fn duration_nanos(duration: Duration) -> Option<u64> {
13    let nanos = duration.as_nanos();
14    if nanos > u64::MAX as u128 {
15        None
16    } else {
17        Some(nanos as u64)
18    }
19}
20
21impl TimePoint {
22    /// The Unix epoch, 1970-01-01T00:00:00Z: the timepoint every other one
23    /// counts nanoseconds from.
24    ///
25    /// `TimePoint::UNIX_EPOCH + Duration::from_secs(secs)` builds a timepoint
26    /// from a Unix seconds value, and
27    /// `t.duration_since(&TimePoint::UNIX_EPOCH)` reads one back.
28    pub const UNIX_EPOCH: TimePoint = TimePoint::new(0);
29
30    /// `self + duration`, or `None` if that is past the end of the
31    /// representable range.
32    pub fn checked_add(&self, duration: Duration) -> Option<TimePoint> {
33        duration_nanos(duration)
34            .and_then(|nanos| (**self).checked_add(nanos))
35            .map(TimePoint::new)
36    }
37
38    /// `self - duration`, or `None` if that is before the Unix epoch.
39    pub fn checked_sub(&self, duration: Duration) -> Option<TimePoint> {
40        duration_nanos(duration)
41            .and_then(|nanos| (**self).checked_sub(nanos))
42            .map(TimePoint::new)
43    }
44
45    /// `self + duration`, or the latest representable timepoint if that
46    /// overflows.
47    ///
48    /// The natural form for a deadline that a caller-supplied span may push
49    /// past the end of the range: the result then simply reads as "never".
50    pub fn saturating_add(&self, duration: Duration) -> TimePoint {
51        match self.checked_add(duration) {
52            Some(sum) => sum,
53            None => TimePoint::new(u64::MAX),
54        }
55    }
56
57    /// `self - duration`, or [`TimePoint::UNIX_EPOCH`] if that would
58    /// precede it.
59    pub fn saturating_sub(&self, duration: Duration) -> TimePoint {
60        match self.checked_sub(duration) {
61            Some(difference) => difference,
62            None => TimePoint::UNIX_EPOCH,
63        }
64    }
65
66    /// The time from `earlier` to `self`, or `Duration::ZERO` if `earlier`
67    /// is in fact later.
68    ///
69    /// The same as `self - earlier`.
70    pub fn duration_since(&self, earlier: &TimePoint) -> Duration {
71        Duration::from_nanos((**self).saturating_sub(**earlier))
72    }
73
74    /// The time from `earlier` to `self`, or `None` if `earlier` is in fact
75    /// later.
76    pub fn checked_duration_since(
77        &self,
78        earlier: &TimePoint,
79    ) -> Option<Duration> {
80        (**self).checked_sub(**earlier).map(Duration::from_nanos)
81    }
82}
83
84impl Add<Duration> for &TimePoint {
85    type Output = TimePoint;
86
87    fn add(self, rhs: Duration) -> TimePoint {
88        self.checked_add(rhs)
89            .expect("overflow when adding duration to timepoint")
90    }
91}
92
93impl Add<Duration> for TimePoint {
94    type Output = TimePoint;
95
96    fn add(self, rhs: Duration) -> TimePoint {
97        &self + rhs
98    }
99}
100
101impl AddAssign<Duration> for TimePoint {
102    fn add_assign(&mut self, rhs: Duration) {
103        *self = &*self + rhs;
104    }
105}
106
107impl Sub<Duration> for &TimePoint {
108    type Output = TimePoint;
109
110    fn sub(self, rhs: Duration) -> TimePoint {
111        self.checked_sub(rhs)
112            .expect("overflow when subtracting duration from timepoint")
113    }
114}
115
116impl Sub<Duration> for TimePoint {
117    type Output = TimePoint;
118
119    fn sub(self, rhs: Duration) -> TimePoint {
120        &self - rhs
121    }
122}
123
124impl SubAssign<Duration> for TimePoint {
125    fn sub_assign(&mut self, rhs: Duration) {
126        *self = &*self - rhs;
127    }
128}
129
130impl Sub<&TimePoint> for &TimePoint {
131    type Output = Duration;
132
133    fn sub(self, rhs: &TimePoint) -> Duration {
134        self.duration_since(rhs)
135    }
136}
137
138impl Sub<TimePoint> for &TimePoint {
139    type Output = Duration;
140
141    fn sub(self, rhs: TimePoint) -> Duration {
142        self.duration_since(&rhs)
143    }
144}
145
146impl Sub<&TimePoint> for TimePoint {
147    type Output = Duration;
148
149    fn sub(self, rhs: &TimePoint) -> Duration {
150        self.duration_since(rhs)
151    }
152}
153
154impl Sub<TimePoint> for TimePoint {
155    type Output = Duration;
156
157    fn sub(self, rhs: TimePoint) -> Duration {
158        self.duration_since(&rhs)
159    }
160}
161
162impl BlockHeight {
163    /// `self + blocks`, or `None` on overflow.
164    pub fn checked_add(&self, blocks: i64) -> Option<BlockHeight> {
165        (**self).checked_add(blocks).map(BlockHeight::new)
166    }
167
168    /// `self - blocks`, or `None` on overflow.
169    pub fn checked_sub(&self, blocks: i64) -> Option<BlockHeight> {
170        (**self).checked_sub(blocks).map(BlockHeight::new)
171    }
172
173    /// The blocks from `earlier` to `self`: negative if `earlier` is in fact
174    /// later.
175    ///
176    /// The same as `self - earlier`. Panics if the difference overflows
177    /// `i64`; see [`checked_blocks_since`](Self::checked_blocks_since).
178    pub fn blocks_since(&self, earlier: &BlockHeight) -> i64 {
179        match self.checked_blocks_since(earlier) {
180            Some(blocks) => blocks,
181            None => panic!("overflow when subtracting block heights"),
182        }
183    }
184
185    /// The blocks from `earlier` to `self`, or `None` if the difference
186    /// overflows `i64`.
187    pub fn checked_blocks_since(&self, earlier: &BlockHeight) -> Option<i64> {
188        (**self).checked_sub(**earlier)
189    }
190}
191
192impl Add<i64> for &BlockHeight {
193    type Output = BlockHeight;
194
195    fn add(self, rhs: i64) -> BlockHeight {
196        self.checked_add(rhs)
197            .expect("overflow when adding blocks to height")
198    }
199}
200
201impl Add<i64> for BlockHeight {
202    type Output = BlockHeight;
203
204    fn add(self, rhs: i64) -> BlockHeight {
205        &self + rhs
206    }
207}
208
209impl AddAssign<i64> for BlockHeight {
210    fn add_assign(&mut self, rhs: i64) {
211        *self = &*self + rhs;
212    }
213}
214
215impl Sub<i64> for &BlockHeight {
216    type Output = BlockHeight;
217
218    fn sub(self, rhs: i64) -> BlockHeight {
219        self.checked_sub(rhs)
220            .expect("overflow when subtracting blocks from height")
221    }
222}
223
224impl Sub<i64> for BlockHeight {
225    type Output = BlockHeight;
226
227    fn sub(self, rhs: i64) -> BlockHeight {
228        &self - rhs
229    }
230}
231
232impl SubAssign<i64> for BlockHeight {
233    fn sub_assign(&mut self, rhs: i64) {
234        *self = &*self - rhs;
235    }
236}
237
238impl Sub<&BlockHeight> for &BlockHeight {
239    type Output = i64;
240
241    fn sub(self, rhs: &BlockHeight) -> i64 {
242        self.blocks_since(rhs)
243    }
244}
245
246impl Sub<BlockHeight> for &BlockHeight {
247    type Output = i64;
248
249    fn sub(self, rhs: BlockHeight) -> i64 {
250        self.blocks_since(&rhs)
251    }
252}
253
254impl Sub<&BlockHeight> for BlockHeight {
255    type Output = i64;
256
257    fn sub(self, rhs: &BlockHeight) -> i64 {
258        self.blocks_since(rhs)
259    }
260}
261
262impl Sub<BlockHeight> for BlockHeight {
263    type Output = i64;
264
265    fn sub(self, rhs: BlockHeight) -> i64 {
266        self.blocks_since(&rhs)
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    const DAY: Duration = Duration::from_hours(24);
275
276    fn timepoint(nanos: u64) -> TimePoint {
277        TimePoint::new(nanos)
278    }
279
280    fn height(blocks: i64) -> BlockHeight {
281        BlockHeight::new(blocks)
282    }
283
284    #[test]
285    fn timepoint_plus_and_minus_duration() {
286        let now = timepoint(1_000);
287        let deadline = &now + Duration::from_nanos(7);
288        assert_eq!(deadline, timepoint(1_007));
289        assert!(now < deadline);
290        assert_eq!(&deadline - Duration::from_nanos(7), now);
291        assert_eq!(deadline.clone() - Duration::from_nanos(7), now);
292
293        let mut moving = now.clone();
294        moving += Duration::from_nanos(10);
295        moving -= Duration::from_nanos(3);
296        assert_eq!(moving, timepoint(1_007));
297
298        assert_eq!(
299            TimePoint::UNIX_EPOCH + Duration::from_secs(5),
300            timepoint(5_000_000_000)
301        );
302        // Sub-nanosecond precision is not a thing: whole nanoseconds only,
303        // and `core` cannot express less than one either.
304        assert_eq!(
305            &now + 7 * DAY,
306            timepoint(1_000 + 7 * 86_400 * 1_000_000_000)
307        );
308    }
309
310    #[test]
311    fn timepoint_difference_saturates_at_zero() {
312        let start = timepoint(100);
313        let end = timepoint(250);
314        assert_eq!(&end - &start, Duration::from_nanos(150));
315        assert_eq!(end.clone() - &start, Duration::from_nanos(150));
316        assert_eq!(&end - start.clone(), Duration::from_nanos(150));
317        assert_eq!(end.clone() - start.clone(), Duration::from_nanos(150));
318        assert_eq!(end.duration_since(&start), Duration::from_nanos(150));
319        assert_eq!(
320            end.checked_duration_since(&start),
321            Some(Duration::from_nanos(150))
322        );
323
324        assert_eq!(&start - &end, Duration::ZERO);
325        assert_eq!(start.duration_since(&end), Duration::ZERO);
326        assert_eq!(start.checked_duration_since(&end), None);
327    }
328
329    #[test]
330    fn timepoint_checked_and_saturating_forms() {
331        let late = timepoint(u64::MAX - 1);
332        assert_eq!(
333            late.checked_add(Duration::from_nanos(1)),
334            Some(timepoint(u64::MAX))
335        );
336        assert_eq!(late.checked_add(Duration::from_nanos(2)), None);
337        assert_eq!(late.saturating_add(DAY), timepoint(u64::MAX));
338        // A duration too long for u64 nanoseconds at all.
339        assert_eq!(timepoint(0).checked_add(Duration::MAX), None);
340        assert_eq!(
341            timepoint(0).saturating_add(Duration::MAX),
342            timepoint(u64::MAX)
343        );
344
345        let early = timepoint(1);
346        assert_eq!(
347            early.checked_sub(Duration::from_nanos(1)),
348            Some(TimePoint::UNIX_EPOCH)
349        );
350        assert_eq!(early.checked_sub(Duration::from_nanos(2)), None);
351        assert_eq!(early.saturating_sub(DAY), TimePoint::UNIX_EPOCH);
352        assert_eq!(early.checked_sub(Duration::MAX), None);
353    }
354
355    #[test]
356    #[should_panic(expected = "overflow when adding duration to timepoint")]
357    fn timepoint_add_overflow_panics() {
358        let _ = timepoint(u64::MAX) + Duration::from_nanos(1);
359    }
360
361    #[test]
362    #[should_panic(
363        expected = "overflow when subtracting duration from timepoint"
364    )]
365    fn timepoint_sub_before_epoch_panics() {
366        let _ = TimePoint::UNIX_EPOCH - Duration::from_nanos(1);
367    }
368
369    #[test]
370    fn height_plus_and_minus_blocks() {
371        let now = height(1_000);
372        let expiry = &now + 100;
373        assert_eq!(expiry, height(1_100));
374        assert!(now < expiry);
375        assert_eq!(&expiry - 100, now);
376        assert_eq!(expiry.clone() - 100, now);
377
378        let mut moving = now.clone();
379        moving += 10;
380        moving -= 3;
381        assert_eq!(moving, height(1_007));
382    }
383
384    #[test]
385    fn height_difference_is_signed() {
386        let earlier = height(100);
387        let later = height(250);
388        assert_eq!(&later - &earlier, 150);
389        assert_eq!(later.clone() - &earlier, 150);
390        assert_eq!(&later - earlier.clone(), 150);
391        assert_eq!(later.clone() - earlier.clone(), 150);
392        assert_eq!(later.blocks_since(&earlier), 150);
393        assert_eq!(&earlier - &later, -150);
394        assert_eq!(earlier.checked_blocks_since(&later), Some(-150));
395        assert_eq!(height(i64::MAX).checked_blocks_since(&height(-1)), None);
396    }
397
398    #[test]
399    fn height_checked_forms() {
400        let top = height(i64::MAX);
401        assert_eq!(top.checked_add(1), None);
402        assert_eq!(top.checked_sub(1), Some(height(i64::MAX - 1)));
403        assert_eq!(height(i64::MIN).checked_sub(1), None);
404    }
405
406    #[test]
407    #[should_panic(expected = "overflow when adding blocks to height")]
408    fn height_add_overflow_panics() {
409        let _ = height(i64::MAX) + 1;
410    }
411
412    #[test]
413    #[should_panic(expected = "overflow when subtracting block heights")]
414    fn height_difference_overflow_panics() {
415        let _ = height(i64::MAX) - height(-1);
416    }
417}