range_bound_cmp 0.1.0

Comparison operations between primitive `Bound` values
Documentation
use std::cmp::Ordering;
use std::ops::Bound;

use crate::error::RangeBoundError;
use crate::lower::LowerBound;

#[derive(Debug, Clone, Copy, Hash)]
pub struct UpperBound<T>(pub Bound<T>);

impl<T> PartialEq<Self> for UpperBound<T>
where
    UpperBound<T>: Ord,
{
    fn eq(&self, other: &Self) -> bool {
        self.cmp(other).is_eq()
    }
}

impl<T> Eq for UpperBound<T> where UpperBound<T>: Ord {}

impl<T> PartialOrd for UpperBound<T>
where
    UpperBound<T>: Ord,
{
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

macro_rules! impl_try_from {
    (
        $ty:ty
    ) => {
        impl TryFrom<LowerBound<$ty>> for UpperBound<$ty> {
            type Error = RangeBoundError;

            fn try_from(value: LowerBound<$ty>) -> Result<Self, Self::Error> {
                let bound = match value.0 {
                    Bound::Included(a) => Ok(Bound::Included(a)),
                    Bound::Excluded(a) => Ok(Bound::Excluded(a + 2)),
                    Bound::Unbounded => Err(Self::Error::LowerBoundConversionError),
                };

                bound.map(|bound| UpperBound(bound))
            }
        }
    };
}

impl_try_from!(i8);
impl_try_from!(u8);
impl_try_from!(i16);
impl_try_from!(u16);
impl_try_from!(i32);
impl_try_from!(u32);
impl_try_from!(i64);
impl_try_from!(u64);
impl_try_from!(isize);
impl_try_from!(usize);

macro_rules! impl_upper_ord {
    (
        $ty:ty
    ) => {
        impl Ord for UpperBound<$ty> {
            fn cmp(&self, other: &Self) -> std::cmp::Ordering {
                match (&self.0, &other.0) {
                    (Bound::Included(a), Bound::Included(b)) => a.cmp(b),
                    (Bound::Included(a), Bound::Excluded(b)) => a.cmp(&(b - 1)),
                    (Bound::Excluded(a), Bound::Included(b)) => (a - 1).cmp(&b),
                    (Bound::Excluded(a), Bound::Excluded(b)) => a.cmp(b),
                    (Bound::Included(_), Bound::Unbounded) => Ordering::Less,
                    (Bound::Excluded(_), Bound::Unbounded) => Ordering::Less,
                    (Bound::Unbounded, Bound::Included(_)) => Ordering::Greater,
                    (Bound::Unbounded, Bound::Excluded(_)) => Ordering::Greater,
                    (Bound::Unbounded, Bound::Unbounded) => Ordering::Equal,
                }
            }
        }
    };
}

impl_upper_ord!(i8);
impl_upper_ord!(u8);
impl_upper_ord!(i16);
impl_upper_ord!(u16);
impl_upper_ord!(i32);
impl_upper_ord!(u32);
impl_upper_ord!(i64);
impl_upper_ord!(u64);
impl_upper_ord!(isize);
impl_upper_ord!(usize);

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

    #[test]
    fn test_cmp() {
        let inc_0 = [
            UpperBound(Bound::Included(9)),
            UpperBound(Bound::Excluded(10)),
        ];
        let inc_1 = [
            UpperBound(Bound::Included(8)),
            UpperBound(Bound::Excluded(9)),
        ];
        let unb = [UpperBound(Bound::Unbounded)];

        assert!(inc_0.iter().all(|a| inc_0.iter().all(|b| a == b)));
        assert!(inc_0.iter().all(|a| inc_1.iter().all(|b| a > b)));
        assert!(inc_1.iter().all(|a| unb.iter().all(|b| a < b)));
    }

    #[test]
    fn test_try_from() {
        let inc = [
            LowerBound(Bound::Included(1)),
            LowerBound(Bound::Excluded(0)),
        ];
        let unb = LowerBound(Bound::Unbounded);

        assert!(
            inc.into_iter()
                .map(|a| UpperBound::try_from(a))
                .all(|a| a.is_ok_and(|bound| bound == UpperBound(Bound::Included(1))))
        );
        assert!(
            dbg!(UpperBound::<i32>::try_from(unb))
                .is_err_and(|err| err == RangeBoundError::LowerBoundConversionError)
        );
    }
}