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::upper::UpperBound;

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

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

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

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

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

            fn try_from(value: UpperBound<$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::UpperBoundConversionError),
                };

                bound.map(|bound| LowerBound(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_lower_ord {
    (
        $ty:ty
    ) => {
        impl Ord for LowerBound<$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::Greater,
                    (Bound::Excluded(_), Bound::Unbounded) => Ordering::Greater,
                    (Bound::Unbounded, Bound::Included(_)) => Ordering::Less,
                    (Bound::Unbounded, Bound::Excluded(_)) => Ordering::Less,
                    (Bound::Unbounded, Bound::Unbounded) => Ordering::Equal,
                }
            }
        }
    };
}

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

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

    #[test]
    fn test_cmp() {
        let inc_0 = [
            LowerBound(Bound::Included(0)),
            LowerBound(Bound::Excluded(-1)),
        ];
        let inc_1 = [
            LowerBound(Bound::Included(1)),
            LowerBound(Bound::Excluded(0)),
        ];
        let unb = [LowerBound(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 = [
            UpperBound(Bound::Included(9)),
            UpperBound(Bound::Excluded(10)),
        ];
        let unb = UpperBound(Bound::Unbounded);

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