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