serial-uint 0.1.1

Generic serial number (RFC 1982) arithmetic with wraparound add/sub and comparison, no_std.
Documentation
//! Serial Number arithmetic following [RFC 1982](https://datatracker.ietf.org/doc/html/rfc1982) semantics.
//!
//! A serial number is a fixed-width unsigned integer whose operations "wrap around" (wraparound).
//! Its biggest difference from an ordinary integer is **comparison**: in the wraparound space `MAX`
//! is the predecessor of `0`, so `Serial(MAX) < Serial(0)`.
//!
//! - Add / subtract an offset: `Serial + T` / `Serial - T`, both wraparound operations.
//! - Wraparound comparison: implements `PartialOrd`. When two serial numbers are exactly half a
//!   cycle apart, their ordering is undefined and `partial_cmp` returns `None` (this is why
//!   `PartialOrd` is used instead of `Ord`).
//!
//! Generics support `u8` / `u16` / `u32` / `u64` / `u128` / `usize` and other widths.

#![no_std]

use core::cmp::Ordering;
use core::fmt;
use core::ops::{Add, AddAssign, Sub, SubAssign};

/// An unsigned integer type that can serve as the underlying storage for a serial number.
///
/// Already implemented for `u8`/`u16`/`u32`/`u64`/`u128`/`usize`; usually no manual implementation
/// is needed.
pub trait SerialPrimitive: Copy + Ord {
    /// The "half cycle" size of the wraparound space, i.e. `2^(BITS-1)`.
    ///
    /// Per RFC 1982 the largest valid addend is `HALF - 1`; `HALF` itself already lies in the
    /// "undefined" region. It is also the dividing point that distinguishes the
    /// "front half / back half" of a cycle during comparison.
    const HALF: Self;
    /// Wraparound addition.
    fn wrapping_add(self, rhs: Self) -> Self;
    /// Wraparound subtraction.
    fn wrapping_sub(self, rhs: Self) -> Self;
}

macro_rules! impl_serial_primitive {
    ($($t:ty),+ $(,)?) => {$(
        impl SerialPrimitive for $t {
            const HALF: Self = 1 << (<$t>::BITS - 1);
            #[inline]
            fn wrapping_add(self, rhs: Self) -> Self { <$t>::wrapping_add(self, rhs) }
            #[inline]
            fn wrapping_sub(self, rhs: Self) -> Self { <$t>::wrapping_sub(self, rhs) }
        }

        /// Compare equality directly against the underlying raw value: `raw == serial`.
        impl PartialEq<Serial<$t>> for $t {
            #[inline]
            fn eq(&self, other: &Serial<$t>) -> bool {
                *self == other.0
            }
        }

        /// Wraparound ordering against a serial number: `raw <cmp> serial`.
        ///
        /// The raw value is treated as `Serial(raw)`, so the comparison uses RFC 1982
        /// wraparound semantics (and yields `None` when half a cycle apart).
        impl PartialOrd<Serial<$t>> for $t {
            #[inline]
            fn partial_cmp(&self, other: &Serial<$t>) -> Option<Ordering> {
                Serial(*self).partial_cmp(other)
            }
        }
    )+};
}

impl_serial_primitive!(u8, u16, u32, u64, u128, usize);

/// A wraparound serial number.
///
/// # Example
///
/// ```
/// use serial_uint::Serial;
///
/// let a = Serial::<u8>::new(250);
/// let b = a + 10; // wraparound: 250 + 10 = 4
/// assert_eq!(b.value(), 4);
/// assert!(a < b); // wraparound comparison: a precedes b
/// ```
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
pub struct Serial<T>(T);

impl<T: SerialPrimitive> Serial<T> {
    /// Constructs a serial number from a raw value.
    #[inline]
    pub const fn new(value: T) -> Self {
        Serial(value)
    }

    /// Returns the underlying raw value.
    #[inline]
    pub fn value(self) -> T {
        self.0
    }

    /// The forward wraparound distance from `self` to `other`, i.e. `other - self` (wraparound).
    ///
    /// The result lies in `[0, MAX]` and is always non-negative. For example, for `u8` the distance
    /// from `250` to `4` is `10`.
    #[inline]
    pub fn distance_to(self, other: Self) -> T {
        other.0.wrapping_sub(self.0)
    }

    /// Returns whether `self` precedes `other` (in the wraparound sense).
    ///
    /// Returns `false` when the two are exactly half a cycle apart and their ordering is undefined.
    #[inline]
    pub fn precedes(self, other: Self) -> bool {
        matches!(self.partial_cmp(&other), Some(Ordering::Less))
    }
}

/// Forwards to the underlying value's `Display`, so a serial number prints just like its raw value.
impl<T: SerialPrimitive + fmt::Display> fmt::Display for Serial<T> {
    #[inline]
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
}

/// Compare equality directly against the underlying raw value: `serial == raw`.
impl<T: SerialPrimitive> PartialEq<T> for Serial<T> {
    #[inline]
    fn eq(&self, other: &T) -> bool {
        self.0 == *other
    }
}

/// Wraparound ordering against a raw value: `serial <cmp> raw`.
///
/// The raw value is treated as `Serial(raw)`, so the comparison uses RFC 1982 wraparound
/// semantics (and yields `None` when half a cycle apart).
impl<T: SerialPrimitive> PartialOrd<T> for Serial<T> {
    #[inline]
    fn partial_cmp(&self, other: &T) -> Option<Ordering> {
        self.partial_cmp(&Serial(*other))
    }
}

impl<T: SerialPrimitive> PartialOrd for Serial<T> {
    /// RFC 1982 wraparound comparison.
    ///
    /// Let the wraparound difference be `d = other - self` (wraparound):
    /// - `d == 0` → equal
    /// - `0 < d < HALF` → `self < other`
    /// - `d > HALF` → `self > other`
    /// - `d == HALF` → undefined, returns `None`
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        if self.0 == other.0 {
            return Some(Ordering::Equal);
        }
        let d = other.0.wrapping_sub(self.0);
        if d == T::HALF {
            None
        } else if d < T::HALF {
            Some(Ordering::Less)
        } else {
            Some(Ordering::Greater)
        }
    }
}

impl<T: SerialPrimitive> Add<T> for Serial<T> {
    type Output = Serial<T>;
    /// Adds an offset (wraparound).
    ///
    /// Per RFC 1982 the addend must be in `[0, HALF - 1]`; larger values are undefined. This is
    /// checked with `debug_assert!` (debug builds only) and otherwise still wraps.
    #[inline]
    fn add(self, rhs: T) -> Serial<T> {
        debug_assert!(rhs < T::HALF, "addend must be < HALF per RFC 1982");
        Serial(self.0.wrapping_add(rhs))
    }
}

impl<T: SerialPrimitive> Sub<T> for Serial<T> {
    type Output = Serial<T>;
    /// Subtracts an offset (wraparound).
    #[inline]
    fn sub(self, rhs: T) -> Serial<T> {
        Serial(self.0.wrapping_sub(rhs))
    }
}

impl<T: SerialPrimitive> AddAssign<T> for Serial<T> {
    #[inline]
    fn add_assign(&mut self, rhs: T) {
        debug_assert!(rhs < T::HALF, "addend must be < HALF per RFC 1982");
        self.0 = self.0.wrapping_add(rhs);
    }
}

impl<T: SerialPrimitive> SubAssign<T> for Serial<T> {
    #[inline]
    fn sub_assign(&mut self, rhs: T) {
        self.0 = self.0.wrapping_sub(rhs);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_wraps_around() {
        let a = Serial::<u8>::new(250);
        assert_eq!((a + 10).value(), 4);
        assert_eq!((a + 5).value(), 255);
        assert_eq!((a + 6).value(), 0);
    }

    #[test]
    fn sub_wraps_around() {
        let a = Serial::<u8>::new(4);
        assert_eq!((a - 10).value(), 250);
        assert_eq!((a - 4).value(), 0);
        assert_eq!((a - 5).value(), 255);
    }

    #[test]
    fn add_assign_sub_assign() {
        let mut a = Serial::<u32>::new(u32::MAX);
        a += 1;
        assert_eq!(a.value(), 0);
        a -= 1;
        assert_eq!(a.value(), u32::MAX);
    }

    #[test]
    fn ordering_basic() {
        let a = Serial::<u32>::new(1);
        let b = Serial::<u32>::new(2);
        assert!(a < b);
        assert!(b > a);
        assert_eq!(a, Serial::<u32>::new(1));
    }

    #[test]
    fn ordering_wraps_around() {
        // MAX is the predecessor of 0
        let max = Serial::<u8>::new(u8::MAX);
        let zero = Serial::<u8>::new(0);
        assert!(max < zero);
        assert!(zero > max);

        // 250 precedes 4 (difference 10, less than the half cycle 128)
        let a = Serial::<u8>::new(250);
        let b = Serial::<u8>::new(4);
        assert!(a < b);
    }

    #[test]
    fn ordering_undefined_at_half() {
        // Exactly half a cycle apart -> undefined
        let a = Serial::<u8>::new(0);
        let b = Serial::<u8>::new(128);
        assert_eq!(a.partial_cmp(&b), None);
        assert_eq!(b.partial_cmp(&a), None);
        assert!(!(a < b));
        assert!(!(a > b));
        assert!(!a.precedes(b));
    }

    #[test]
    fn distance_to_is_forward_and_nonnegative() {
        let a = Serial::<u8>::new(250);
        let b = Serial::<u8>::new(4);
        assert_eq!(a.distance_to(b), 10);
        assert_eq!(b.distance_to(a), 246);
    }

    #[test]
    fn eq_with_raw_value_both_directions() {
        let a = Serial::<u32>::new(42);
        // Forward: serial compared against raw value
        assert!(a == 42);
        assert!(a != 7);
        // Reverse: raw value compared against serial
        assert!(42u32 == a);
        assert!(7u32 != a);
        // Different widths
        assert!(Serial::<u8>::new(255) == 255u8);
        assert!(0u16 != Serial::<u16>::new(1));
    }

    #[test]
    fn ord_with_raw_value_both_directions() {
        let a = Serial::<u8>::new(250);
        // serial <cmp> raw, wraparound: 250 precedes 4 (distance 10)
        assert!(a < 4u8);
        assert!(a <= 4u8);
        assert!(a > 200u8);
        // raw <cmp> serial, symmetric
        assert!(4u8 > a);
        assert!(200u8 < a);
        // MAX precedes 0
        assert!(Serial::<u8>::new(u8::MAX) < 0u8);
        assert!(0u8 > Serial::<u8>::new(u8::MAX));
    }

    #[test]
    fn ord_with_raw_value_undefined_at_half() {
        let a = Serial::<u8>::new(0);
        // exactly half a cycle apart -> undefined
        assert_eq!(a.partial_cmp(&128u8), None);
        assert_eq!(128u8.partial_cmp(&a), None);
        assert!(!(a < 128u8));
        assert!(!(a > 128u8));
    }

    #[test]
    fn works_across_widths() {
        assert!(Serial::<u16>::new(u16::MAX) < Serial::<u16>::new(0));
        assert!(Serial::<u64>::new(u64::MAX) < Serial::<u64>::new(0));
        assert_eq!((Serial::<u128>::new(u128::MAX) + 1).value(), 0);
    }
}