Skip to main content

zerodds_qos/
duration.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! DDS `Duration_t` (DDSI-RTPS §9.3.2) — i32 seconds + u32 fraction.
4
5use zerodds_cdr::{BufferReader, BufferWriter, DecodeError, EncodeError};
6
7/// DDS Duration (signed seconds + unsigned 2^-32-fractions).
8///
9/// The spec defines two special values:
10/// - `DURATION_INFINITE`: `{ seconds: 0x7FFFFFFF, fraction: 0xFFFFFFFF }`.
11/// - `DURATION_ZERO`: `{ seconds: 0, fraction: 0 }`.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct Duration {
14    /// Sekundenanteil (signed 32 bit).
15    pub seconds: i32,
16    /// Bruchteil-Anteil (2^-32-Sekunden).
17    pub fraction: u32,
18}
19
20impl Duration {
21    /// Spec §9.3.2: `DURATION_INFINITE`.
22    pub const INFINITE: Self = Self {
23        seconds: i32::MAX,
24        fraction: u32::MAX,
25    };
26
27    /// Spec §9.3.2: `DURATION_ZERO`.
28    pub const ZERO: Self = Self {
29        seconds: 0,
30        fraction: 0,
31    };
32
33    /// Creates a `Duration` from a number of seconds.
34    #[must_use]
35    pub const fn from_secs(seconds: i32) -> Self {
36        Self {
37            seconds,
38            fraction: 0,
39        }
40    }
41
42    /// Creates a `Duration` from a number of milliseconds.
43    ///
44    /// Time is represented as `{ seconds: i32, fraction: u32 (2^-32 s) }`
45    /// — `fraction` is **unsigned**. Negative durations describe the time
46    /// via negative `seconds` + positive `fraction`, so that
47    /// `(seconds + fraction*2^-32)` runs continuously across 0. With
48    /// `div_euclid/rem_euclid` the remainder always stays `[0, 1000)`.
49    ///
50    /// Examples:
51    /// - `from_millis(1500)` = `{1, 2^31}` (1.5 s).
52    /// - `from_millis(-500)` = `{-1, 2^31}` (= -1 + 0.5 = -0.5 s).
53    /// - `from_millis(-1500)` = `{-2, 2^31}` (= -2 + 0.5 = -1.5 s).
54    #[must_use]
55    pub const fn from_millis(ms: i32) -> Self {
56        let seconds = ms.div_euclid(1000);
57        let remainder_ms = ms.rem_euclid(1000) as u32; // in [0, 1000)
58        let fraction = ((remainder_ms as u64 * (1u64 << 32)) / 1000) as u32;
59        Self { seconds, fraction }
60    }
61
62    /// Creates a `Duration` from a number of microseconds. Sub-millisecond
63    /// precision is preserved via the RTPS `fraction` field (latency budgets,
64    /// deadlines, time-based filters routinely use µs). Same negative-value
65    /// convention as [`Self::from_millis`].
66    #[must_use]
67    pub const fn from_micros(us: i32) -> Self {
68        let seconds = us.div_euclid(1_000_000);
69        let remainder_us = us.rem_euclid(1_000_000) as u32; // in [0, 1_000_000)
70        let fraction = ((remainder_us as u64 * (1u64 << 32)) / 1_000_000) as u32;
71        Self { seconds, fraction }
72    }
73
74    /// `true` if `self == INFINITE`.
75    #[must_use]
76    pub const fn is_infinite(self) -> bool {
77        self.seconds == i32::MAX && self.fraction == u32::MAX
78    }
79
80    /// Converts into nanoseconds. INFINITE and negative durations
81    /// return `u128::MAX` or saturate to `0` (the caller treats
82    /// `u128::MAX` as "never expires").
83    #[must_use]
84    pub const fn to_nanos(self) -> u128 {
85        if self.is_infinite() {
86            return u128::MAX;
87        }
88        if self.seconds < 0 {
89            return 0;
90        }
91        let secs = self.seconds as u128;
92        // fraction is in 2^-32 seconds: nanos = fraction * 1e9 / 2^32.
93        let frac_nanos = (self.fraction as u128 * 1_000_000_000) >> 32;
94        secs * 1_000_000_000 + frac_nanos
95    }
96
97    /// `true` if `self == ZERO`.
98    #[must_use]
99    pub const fn is_zero(self) -> bool {
100        self.seconds == 0 && self.fraction == 0
101    }
102
103    /// Wire-Encoding: `{ i32 seconds; u32 fraction }` (8 byte).
104    ///
105    /// # Errors
106    /// Buffer-Overflow.
107    pub fn encode_into(self, w: &mut BufferWriter) -> Result<(), EncodeError> {
108        w.write_u32(self.seconds as u32)?;
109        w.write_u32(self.fraction)
110    }
111
112    /// Wire-Decoding.
113    ///
114    /// # Errors
115    /// Buffer-Underflow.
116    pub fn decode_from(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
117        let seconds = r.read_u32()? as i32;
118        let fraction = r.read_u32()?;
119        Ok(Self { seconds, fraction })
120    }
121
122    /// 8-byte array (LE) — useful for in-place copies in PL_CDR values.
123    #[must_use]
124    pub fn to_bytes_le(self) -> [u8; 8] {
125        let mut out = [0u8; 8];
126        out[..4].copy_from_slice(&self.seconds.to_le_bytes());
127        out[4..].copy_from_slice(&self.fraction.to_le_bytes());
128        out
129    }
130
131    /// 8-byte array (BE) — for PL_CDR_BE payloads like the handshake `c.pdata`.
132    #[must_use]
133    pub fn to_bytes_be(self) -> [u8; 8] {
134        let mut out = [0u8; 8];
135        out[..4].copy_from_slice(&self.seconds.to_be_bytes());
136        out[4..].copy_from_slice(&self.fraction.to_be_bytes());
137        out
138    }
139
140    /// Aus 8-byte-LE-Array.
141    #[must_use]
142    pub fn from_bytes_le(bytes: [u8; 8]) -> Self {
143        let seconds = i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
144        let fraction = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
145        Self { seconds, fraction }
146    }
147}
148
149impl Default for Duration {
150    fn default() -> Self {
151        Self::ZERO
152    }
153}
154
155#[cfg(test)]
156#[allow(clippy::unwrap_used)]
157mod tests {
158    use super::*;
159    use zerodds_cdr::Endianness;
160
161    #[test]
162    fn infinite_constant_matches_spec() {
163        assert_eq!(Duration::INFINITE.seconds, i32::MAX);
164        assert_eq!(Duration::INFINITE.fraction, u32::MAX);
165        assert!(Duration::INFINITE.is_infinite());
166    }
167
168    #[test]
169    fn zero_is_default_and_zero() {
170        assert_eq!(Duration::default(), Duration::ZERO);
171        assert!(Duration::ZERO.is_zero());
172    }
173
174    #[test]
175    fn from_secs_has_zero_fraction() {
176        let d = Duration::from_secs(42);
177        assert_eq!(d.seconds, 42);
178        assert_eq!(d.fraction, 0);
179    }
180
181    #[test]
182    fn from_millis_splits_correctly() {
183        let d = Duration::from_millis(1500);
184        assert_eq!(d.seconds, 1);
185        // 500ms -> 500/1000 * 2^32 = 2_147_483_648
186        assert_eq!(d.fraction, 2_147_483_648);
187    }
188
189    #[test]
190    fn from_micros_keeps_sub_millisecond_precision() {
191        // 1.5 s expressed in µs.
192        let d = Duration::from_micros(1_500_000);
193        assert_eq!(d.seconds, 1);
194        assert_eq!(d.fraction, 2_147_483_648);
195        // 500 µs — below millisecond granularity, must not collapse to zero.
196        let half_ms = Duration::from_micros(500);
197        assert_eq!(half_ms.seconds, 0);
198        assert!(half_ms.fraction > 0);
199        // 500 µs == 0.5 ms; same fraction as from_millis would give for 0.5ms.
200        assert_eq!(
201            half_ms.fraction,
202            ((500u64 * (1u64 << 32)) / 1_000_000) as u32
203        );
204    }
205
206    #[test]
207    fn encode_decode_roundtrip() {
208        let d = Duration {
209            seconds: 7,
210            fraction: 0xCAFE_BABE,
211        };
212        let mut w = BufferWriter::new(Endianness::Little);
213        d.encode_into(&mut w).unwrap();
214        let bytes = w.into_bytes();
215        assert_eq!(bytes.len(), 8);
216        let mut r = BufferReader::new(&bytes, Endianness::Little);
217        let back = Duration::decode_from(&mut r).unwrap();
218        assert_eq!(back, d);
219    }
220
221    #[test]
222    fn to_from_bytes_le_roundtrip() {
223        let d = Duration {
224            seconds: -3,
225            fraction: 0xDEAD_BEEF,
226        };
227        let bytes = d.to_bytes_le();
228        let back = Duration::from_bytes_le(bytes);
229        assert_eq!(back, d);
230    }
231
232    #[test]
233    fn ord_compares_seconds_then_fraction() {
234        let a = Duration {
235            seconds: 1,
236            fraction: 0,
237        };
238        let b = Duration {
239            seconds: 1,
240            fraction: 1,
241        };
242        let c = Duration {
243            seconds: 2,
244            fraction: 0,
245        };
246        assert!(a < b);
247        assert!(b < c);
248    }
249}