Skip to main content

serial_uint/
lib.rs

1//! Serial Number arithmetic following [RFC 1982](https://datatracker.ietf.org/doc/html/rfc1982) semantics.
2//!
3//! A serial number is a fixed-width unsigned integer whose operations "wrap around" (wraparound).
4//! Its biggest difference from an ordinary integer is **comparison**: in the wraparound space `MAX`
5//! is the predecessor of `0`, so `Serial(MAX) < Serial(0)`.
6//!
7//! - Add / subtract an offset: `Serial + T` / `Serial - T`, both wraparound operations.
8//! - Wraparound comparison: implements `PartialOrd`. When two serial numbers are exactly half a
9//!   cycle apart, their ordering is undefined and `partial_cmp` returns `None` (this is why
10//!   `PartialOrd` is used instead of `Ord`).
11//!
12//! Generics support `u8` / `u16` / `u32` / `u64` / `u128` / `usize` and other widths.
13
14#![no_std]
15
16use core::cmp::Ordering;
17use core::fmt;
18use core::ops::{Add, AddAssign, Sub, SubAssign};
19
20/// An unsigned integer type that can serve as the underlying storage for a serial number.
21///
22/// Already implemented for `u8`/`u16`/`u32`/`u64`/`u128`/`usize`; usually no manual implementation
23/// is needed.
24pub trait SerialPrimitive: Copy + Ord {
25    /// The "half cycle" size of the wraparound space, i.e. `2^(BITS-1)`.
26    ///
27    /// Per RFC 1982 the largest valid addend is `HALF - 1`; `HALF` itself already lies in the
28    /// "undefined" region. It is also the dividing point that distinguishes the
29    /// "front half / back half" of a cycle during comparison.
30    const HALF: Self;
31    /// Wraparound addition.
32    fn wrapping_add(self, rhs: Self) -> Self;
33    /// Wraparound subtraction.
34    fn wrapping_sub(self, rhs: Self) -> Self;
35}
36
37macro_rules! impl_serial_primitive {
38    ($($t:ty),+ $(,)?) => {$(
39        impl SerialPrimitive for $t {
40            const HALF: Self = 1 << (<$t>::BITS - 1);
41            #[inline]
42            fn wrapping_add(self, rhs: Self) -> Self { <$t>::wrapping_add(self, rhs) }
43            #[inline]
44            fn wrapping_sub(self, rhs: Self) -> Self { <$t>::wrapping_sub(self, rhs) }
45        }
46
47        /// Compare equality directly against the underlying raw value: `raw == serial`.
48        impl PartialEq<Serial<$t>> for $t {
49            #[inline]
50            fn eq(&self, other: &Serial<$t>) -> bool {
51                *self == other.0
52            }
53        }
54
55        /// Wraparound ordering against a serial number: `raw <cmp> serial`.
56        ///
57        /// The raw value is treated as `Serial(raw)`, so the comparison uses RFC 1982
58        /// wraparound semantics (and yields `None` when half a cycle apart).
59        impl PartialOrd<Serial<$t>> for $t {
60            #[inline]
61            fn partial_cmp(&self, other: &Serial<$t>) -> Option<Ordering> {
62                Serial(*self).partial_cmp(other)
63            }
64        }
65    )+};
66}
67
68impl_serial_primitive!(u8, u16, u32, u64, u128, usize);
69
70/// A wraparound serial number.
71///
72/// # Example
73///
74/// ```
75/// use serial_uint::Serial;
76///
77/// let a = Serial::<u8>::new(250);
78/// let b = a + 10; // wraparound: 250 + 10 = 4
79/// assert_eq!(b.value(), 4);
80/// assert!(a < b); // wraparound comparison: a precedes b
81/// ```
82#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
83pub struct Serial<T>(T);
84
85impl<T: SerialPrimitive> Serial<T> {
86    /// Constructs a serial number from a raw value.
87    #[inline]
88    pub const fn new(value: T) -> Self {
89        Serial(value)
90    }
91
92    /// Returns the underlying raw value.
93    #[inline]
94    pub fn value(self) -> T {
95        self.0
96    }
97
98    /// The forward wraparound distance from `self` to `other`, i.e. `other - self` (wraparound).
99    ///
100    /// The result lies in `[0, MAX]` and is always non-negative. For example, for `u8` the distance
101    /// from `250` to `4` is `10`.
102    #[inline]
103    pub fn distance_to(self, other: Self) -> T {
104        other.0.wrapping_sub(self.0)
105    }
106
107    /// Returns whether `self` precedes `other` (in the wraparound sense).
108    ///
109    /// Returns `false` when the two are exactly half a cycle apart and their ordering is undefined.
110    #[inline]
111    pub fn precedes(self, other: Self) -> bool {
112        matches!(self.partial_cmp(&other), Some(Ordering::Less))
113    }
114}
115
116/// Forwards to the underlying value's `Display`, so a serial number prints just like its raw value.
117impl<T: SerialPrimitive + fmt::Display> fmt::Display for Serial<T> {
118    #[inline]
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        self.0.fmt(f)
121    }
122}
123
124/// Compare equality directly against the underlying raw value: `serial == raw`.
125impl<T: SerialPrimitive> PartialEq<T> for Serial<T> {
126    #[inline]
127    fn eq(&self, other: &T) -> bool {
128        self.0 == *other
129    }
130}
131
132/// Wraparound ordering against a raw value: `serial <cmp> raw`.
133///
134/// The raw value is treated as `Serial(raw)`, so the comparison uses RFC 1982 wraparound
135/// semantics (and yields `None` when half a cycle apart).
136impl<T: SerialPrimitive> PartialOrd<T> for Serial<T> {
137    #[inline]
138    fn partial_cmp(&self, other: &T) -> Option<Ordering> {
139        self.partial_cmp(&Serial(*other))
140    }
141}
142
143impl<T: SerialPrimitive> PartialOrd for Serial<T> {
144    /// RFC 1982 wraparound comparison.
145    ///
146    /// Let the wraparound difference be `d = other - self` (wraparound):
147    /// - `d == 0` → equal
148    /// - `0 < d < HALF` → `self < other`
149    /// - `d > HALF` → `self > other`
150    /// - `d == HALF` → undefined, returns `None`
151    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
152        if self.0 == other.0 {
153            return Some(Ordering::Equal);
154        }
155        let d = other.0.wrapping_sub(self.0);
156        if d == T::HALF {
157            None
158        } else if d < T::HALF {
159            Some(Ordering::Less)
160        } else {
161            Some(Ordering::Greater)
162        }
163    }
164}
165
166impl<T: SerialPrimitive> Add<T> for Serial<T> {
167    type Output = Serial<T>;
168    /// Adds an offset (wraparound).
169    ///
170    /// Per RFC 1982 the addend must be in `[0, HALF - 1]`; larger values are undefined. This is
171    /// checked with `debug_assert!` (debug builds only) and otherwise still wraps.
172    #[inline]
173    fn add(self, rhs: T) -> Serial<T> {
174        debug_assert!(rhs < T::HALF, "addend must be < HALF per RFC 1982");
175        Serial(self.0.wrapping_add(rhs))
176    }
177}
178
179impl<T: SerialPrimitive> Sub<T> for Serial<T> {
180    type Output = Serial<T>;
181    /// Subtracts an offset (wraparound).
182    #[inline]
183    fn sub(self, rhs: T) -> Serial<T> {
184        Serial(self.0.wrapping_sub(rhs))
185    }
186}
187
188impl<T: SerialPrimitive> AddAssign<T> for Serial<T> {
189    #[inline]
190    fn add_assign(&mut self, rhs: T) {
191        debug_assert!(rhs < T::HALF, "addend must be < HALF per RFC 1982");
192        self.0 = self.0.wrapping_add(rhs);
193    }
194}
195
196impl<T: SerialPrimitive> SubAssign<T> for Serial<T> {
197    #[inline]
198    fn sub_assign(&mut self, rhs: T) {
199        self.0 = self.0.wrapping_sub(rhs);
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn add_wraps_around() {
209        let a = Serial::<u8>::new(250);
210        assert_eq!((a + 10).value(), 4);
211        assert_eq!((a + 5).value(), 255);
212        assert_eq!((a + 6).value(), 0);
213    }
214
215    #[test]
216    fn sub_wraps_around() {
217        let a = Serial::<u8>::new(4);
218        assert_eq!((a - 10).value(), 250);
219        assert_eq!((a - 4).value(), 0);
220        assert_eq!((a - 5).value(), 255);
221    }
222
223    #[test]
224    fn add_assign_sub_assign() {
225        let mut a = Serial::<u32>::new(u32::MAX);
226        a += 1;
227        assert_eq!(a.value(), 0);
228        a -= 1;
229        assert_eq!(a.value(), u32::MAX);
230    }
231
232    #[test]
233    fn ordering_basic() {
234        let a = Serial::<u32>::new(1);
235        let b = Serial::<u32>::new(2);
236        assert!(a < b);
237        assert!(b > a);
238        assert_eq!(a, Serial::<u32>::new(1));
239    }
240
241    #[test]
242    fn ordering_wraps_around() {
243        // MAX is the predecessor of 0
244        let max = Serial::<u8>::new(u8::MAX);
245        let zero = Serial::<u8>::new(0);
246        assert!(max < zero);
247        assert!(zero > max);
248
249        // 250 precedes 4 (difference 10, less than the half cycle 128)
250        let a = Serial::<u8>::new(250);
251        let b = Serial::<u8>::new(4);
252        assert!(a < b);
253    }
254
255    #[test]
256    fn ordering_undefined_at_half() {
257        // Exactly half a cycle apart -> undefined
258        let a = Serial::<u8>::new(0);
259        let b = Serial::<u8>::new(128);
260        assert_eq!(a.partial_cmp(&b), None);
261        assert_eq!(b.partial_cmp(&a), None);
262        assert!(!(a < b));
263        assert!(!(a > b));
264        assert!(!a.precedes(b));
265    }
266
267    #[test]
268    fn distance_to_is_forward_and_nonnegative() {
269        let a = Serial::<u8>::new(250);
270        let b = Serial::<u8>::new(4);
271        assert_eq!(a.distance_to(b), 10);
272        assert_eq!(b.distance_to(a), 246);
273    }
274
275    #[test]
276    fn eq_with_raw_value_both_directions() {
277        let a = Serial::<u32>::new(42);
278        // Forward: serial compared against raw value
279        assert!(a == 42);
280        assert!(a != 7);
281        // Reverse: raw value compared against serial
282        assert!(42u32 == a);
283        assert!(7u32 != a);
284        // Different widths
285        assert!(Serial::<u8>::new(255) == 255u8);
286        assert!(0u16 != Serial::<u16>::new(1));
287    }
288
289    #[test]
290    fn ord_with_raw_value_both_directions() {
291        let a = Serial::<u8>::new(250);
292        // serial <cmp> raw, wraparound: 250 precedes 4 (distance 10)
293        assert!(a < 4u8);
294        assert!(a <= 4u8);
295        assert!(a > 200u8);
296        // raw <cmp> serial, symmetric
297        assert!(4u8 > a);
298        assert!(200u8 < a);
299        // MAX precedes 0
300        assert!(Serial::<u8>::new(u8::MAX) < 0u8);
301        assert!(0u8 > Serial::<u8>::new(u8::MAX));
302    }
303
304    #[test]
305    fn ord_with_raw_value_undefined_at_half() {
306        let a = Serial::<u8>::new(0);
307        // exactly half a cycle apart -> undefined
308        assert_eq!(a.partial_cmp(&128u8), None);
309        assert_eq!(128u8.partial_cmp(&a), None);
310        assert!(!(a < 128u8));
311        assert!(!(a > 128u8));
312    }
313
314    #[test]
315    fn works_across_widths() {
316        assert!(Serial::<u16>::new(u16::MAX) < Serial::<u16>::new(0));
317        assert!(Serial::<u64>::new(u64::MAX) < Serial::<u64>::new(0));
318        assert_eq!((Serial::<u128>::new(u128::MAX) + 1).value(), 0);
319    }
320}