use core::cmp::Ordering;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct SerialNumber16(pub u16);
impl SerialNumber16 {
pub const HALF_WINDOW: u16 = 1u16 << 15;
#[must_use]
pub const fn new(value: u16) -> Self {
Self(value)
}
#[must_use]
pub const fn raw(self) -> u16 {
self.0
}
#[must_use]
pub fn next(self) -> Self {
Self(self.0.wrapping_add(1))
}
#[must_use]
pub fn wrapping_sub(self, rhs: Self) -> u16 {
self.0.wrapping_sub(rhs.0)
}
#[must_use]
pub fn wrapping_diff(self, rhs: Self) -> i32 {
let raw_diff = self.0.wrapping_sub(rhs.0); match raw_diff.cmp(&Self::HALF_WINDOW) {
core::cmp::Ordering::Less | core::cmp::Ordering::Equal => i32::from(raw_diff),
core::cmp::Ordering::Greater => i32::from(raw_diff) - (1i32 << 16),
}
}
#[must_use]
pub fn is_undefined_pair(self, rhs: Self) -> bool {
self.0.wrapping_sub(rhs.0) == Self::HALF_WINDOW
}
#[must_use]
pub fn wrapping_lt(self, rhs: Self) -> bool {
let d = rhs.0.wrapping_sub(self.0);
d > 0 && d < Self::HALF_WINDOW
}
#[must_use]
pub fn wrapping_gt(self, rhs: Self) -> bool {
rhs.wrapping_lt(self)
}
#[must_use]
pub fn wrapping_cmp(self, rhs: Self) -> Option<Ordering> {
if self.0 == rhs.0 {
Some(Ordering::Equal)
} else if self.is_undefined_pair(rhs) {
None
} else if self.wrapping_lt(rhs) {
Some(Ordering::Less)
} else {
Some(Ordering::Greater)
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
#[test]
fn next_increments_by_one() {
let s = SerialNumber16::new(5);
assert_eq!(s.next().raw(), 6);
}
#[test]
fn next_wraps_at_u16_max() {
let s = SerialNumber16::new(u16::MAX);
assert_eq!(s.next().raw(), 0);
}
#[test]
fn lt_simple_case() {
let a = SerialNumber16::new(10);
let b = SerialNumber16::new(20);
assert!(a.wrapping_lt(b));
assert!(!b.wrapping_lt(a));
}
#[test]
fn lt_across_wrap_boundary() {
let a = SerialNumber16::new(u16::MAX - 5);
let b = SerialNumber16::new(3);
assert!(a.wrapping_lt(b), "wrap-around: u16::MAX-5 < 3");
assert!(!b.wrapping_lt(a));
}
#[test]
fn gt_is_inverse_of_lt() {
let a = SerialNumber16::new(100);
let b = SerialNumber16::new(200);
assert!(b.wrapping_gt(a));
assert!(!a.wrapping_gt(b));
}
#[test]
fn equal_serial_numbers_neither_lt_nor_gt() {
let a = SerialNumber16::new(42);
let b = SerialNumber16::new(42);
assert!(!a.wrapping_lt(b));
assert!(!a.wrapping_gt(b));
assert_eq!(a.wrapping_cmp(b), Some(Ordering::Equal));
}
#[test]
fn undefined_pair_at_exactly_half_window() {
let a = SerialNumber16::new(0);
let b = SerialNumber16::new(SerialNumber16::HALF_WINDOW);
assert!(a.is_undefined_pair(b));
assert!(b.is_undefined_pair(a));
assert_eq!(a.wrapping_cmp(b), None);
}
#[test]
fn diff_signed_within_window() {
let a = SerialNumber16::new(10);
let b = SerialNumber16::new(3);
assert_eq!(a.wrapping_diff(b), 7);
assert_eq!(b.wrapping_diff(a), -7);
}
#[test]
fn diff_across_wrap_yields_signed_value() {
let a = SerialNumber16::new(u16::MAX);
let b = SerialNumber16::new(0);
assert_eq!(a.wrapping_diff(b), -1);
assert_eq!(b.wrapping_diff(a), 1);
}
#[test]
fn wrap_around_does_not_break_lt_for_consecutive_numbers() {
let a = SerialNumber16::new(u16::MAX);
let b = a.next();
assert!(a.wrapping_lt(b));
assert!(b.wrapping_gt(a));
}
}