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/// Die Spec kennt zwei Spezialwerte:
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    /// Erzeugt `Duration` aus einer Sekundenzahl.
34    #[must_use]
35    pub const fn from_secs(seconds: i32) -> Self {
36        Self {
37            seconds,
38            fraction: 0,
39        }
40    }
41
42    /// Erzeugt `Duration` aus einer Millisekundenzahl.
43    ///
44    /// Zeit ist als `{ seconds: i32, fraction: u32 (2^-32 s) }` repraesentiert
45    /// — `fraction` ist **unsigned**. Negative Durations beschreiben die Zeit
46    /// durch negative `seconds` + positive `fraction`, so dass
47    /// `(seconds + fraction*2^-32)` stetig ueber 0 hinweg laeuft. Mit
48    /// `div_euclid/rem_euclid` bleibt der Remainder immer `[0, 1000)`.
49    ///
50    /// Beispiele:
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    /// `true` wenn `self == INFINITE`.
63    #[must_use]
64    pub const fn is_infinite(self) -> bool {
65        self.seconds == i32::MAX && self.fraction == u32::MAX
66    }
67
68    /// Konvertiert in Nanosekunden. INFINITE und negative Durations
69    /// liefern `u128::MAX` bzw. saturieren auf `0` (Caller behandelt
70    /// `u128::MAX` als "nie ablaufen").
71    #[must_use]
72    pub const fn to_nanos(self) -> u128 {
73        if self.is_infinite() {
74            return u128::MAX;
75        }
76        if self.seconds < 0 {
77            return 0;
78        }
79        let secs = self.seconds as u128;
80        // fraction ist 2^-32-Sekunden: nanos = fraction * 1e9 / 2^32.
81        let frac_nanos = (self.fraction as u128 * 1_000_000_000) >> 32;
82        secs * 1_000_000_000 + frac_nanos
83    }
84
85    /// `true` wenn `self == ZERO`.
86    #[must_use]
87    pub const fn is_zero(self) -> bool {
88        self.seconds == 0 && self.fraction == 0
89    }
90
91    /// Wire-Encoding: `{ i32 seconds; u32 fraction }` (8 byte).
92    ///
93    /// # Errors
94    /// Buffer-Overflow.
95    pub fn encode_into(self, w: &mut BufferWriter) -> Result<(), EncodeError> {
96        w.write_u32(self.seconds as u32)?;
97        w.write_u32(self.fraction)
98    }
99
100    /// Wire-Decoding.
101    ///
102    /// # Errors
103    /// Buffer-Underflow.
104    pub fn decode_from(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
105        let seconds = r.read_u32()? as i32;
106        let fraction = r.read_u32()?;
107        Ok(Self { seconds, fraction })
108    }
109
110    /// 8-byte-Array (LE) — nuetzlich fuer in-place-Copies in PL_CDR-Values.
111    #[must_use]
112    pub fn to_bytes_le(self) -> [u8; 8] {
113        let mut out = [0u8; 8];
114        out[..4].copy_from_slice(&self.seconds.to_le_bytes());
115        out[4..].copy_from_slice(&self.fraction.to_le_bytes());
116        out
117    }
118
119    /// Aus 8-byte-LE-Array.
120    #[must_use]
121    pub fn from_bytes_le(bytes: [u8; 8]) -> Self {
122        let seconds = i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
123        let fraction = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
124        Self { seconds, fraction }
125    }
126}
127
128impl Default for Duration {
129    fn default() -> Self {
130        Self::ZERO
131    }
132}
133
134#[cfg(test)]
135#[allow(clippy::unwrap_used)]
136mod tests {
137    use super::*;
138    use zerodds_cdr::Endianness;
139
140    #[test]
141    fn infinite_constant_matches_spec() {
142        assert_eq!(Duration::INFINITE.seconds, i32::MAX);
143        assert_eq!(Duration::INFINITE.fraction, u32::MAX);
144        assert!(Duration::INFINITE.is_infinite());
145    }
146
147    #[test]
148    fn zero_is_default_and_zero() {
149        assert_eq!(Duration::default(), Duration::ZERO);
150        assert!(Duration::ZERO.is_zero());
151    }
152
153    #[test]
154    fn from_secs_has_zero_fraction() {
155        let d = Duration::from_secs(42);
156        assert_eq!(d.seconds, 42);
157        assert_eq!(d.fraction, 0);
158    }
159
160    #[test]
161    fn from_millis_splits_correctly() {
162        let d = Duration::from_millis(1500);
163        assert_eq!(d.seconds, 1);
164        // 500ms -> 500/1000 * 2^32 = 2_147_483_648
165        assert_eq!(d.fraction, 2_147_483_648);
166    }
167
168    #[test]
169    fn encode_decode_roundtrip() {
170        let d = Duration {
171            seconds: 7,
172            fraction: 0xCAFE_BABE,
173        };
174        let mut w = BufferWriter::new(Endianness::Little);
175        d.encode_into(&mut w).unwrap();
176        let bytes = w.into_bytes();
177        assert_eq!(bytes.len(), 8);
178        let mut r = BufferReader::new(&bytes, Endianness::Little);
179        let back = Duration::decode_from(&mut r).unwrap();
180        assert_eq!(back, d);
181    }
182
183    #[test]
184    fn to_from_bytes_le_roundtrip() {
185        let d = Duration {
186            seconds: -3,
187            fraction: 0xDEAD_BEEF,
188        };
189        let bytes = d.to_bytes_le();
190        let back = Duration::from_bytes_le(bytes);
191        assert_eq!(back, d);
192    }
193
194    #[test]
195    fn ord_compares_seconds_then_fraction() {
196        let a = Duration {
197            seconds: 1,
198            fraction: 0,
199        };
200        let b = Duration {
201            seconds: 1,
202            fraction: 1,
203        };
204        let c = Duration {
205            seconds: 2,
206            fraction: 0,
207        };
208        assert!(a < b);
209        assert!(b < c);
210    }
211}