checked_clamp 0.1.0

Clamp alternative that returns a result instead of panicking
Documentation
//! Clamp alternative that returns a result instead of panicking
#![doc = include_str!("../README.md")]
#![no_std]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(clippy::unwrap_used)]
#![warn(clippy::wildcard_imports)]
#![warn(clippy::enum_glob_use)]

#[cfg(feature = "std")]
extern crate std;

/// Provides a method to try to clamp a value between a minimum and a maximum.
pub trait CheckedClamp: PartialOrd + Sized {
    /// Tries to clamp `self` between `min` and `max`, returning an error if it is not possible.
    fn checked_clamp(
        self,
        min: impl Into<Self>,
        max: impl Into<Self>,
    ) -> Result<Self, CheckedClampError> {
        checked_clamp_mono(self, min.into(), max.into())
    }
}

/// Error returned by the `checked_clamp` method when it fails to clamp the value.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum CheckedClampError {
    /// Min is greater than max
    MinGreaterThanMax,
    /// Failed to compare values
    ComparisonFailed,
}

// Blanked implementation
impl<T> CheckedClamp for T where T: PartialOrd + Sized {}

impl core::fmt::Display for CheckedClampError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::MinGreaterThanMax => f.write_str("Min is greater than max"),
            Self::ComparisonFailed => f.write_str("Failed to compare values"),
        }
    }
}

fn checked_clamp_mono<T>(value: T, min: T, max: T) -> Result<T, CheckedClampError>
where
    T: PartialOrd,
{
    use core::cmp::Ordering as O;
    use CheckedClampError as E;
    match min.partial_cmp(&max) {
        None => Err(E::ComparisonFailed),
        Some(O::Greater) => Err(E::MinGreaterThanMax),
        _ => Ok(()),
    }?;
    let (min_cmp, max_cmp) = (
        value.partial_cmp(&min).ok_or(E::ComparisonFailed)?,
        value.partial_cmp(&max).ok_or(E::ComparisonFailed)?,
    );
    match (min_cmp, max_cmp) {
        (O::Less, _) => Ok(min),
        (_, O::Greater) => Ok(max),
        (_, _) => Ok(value),
    }
}

#[cfg(feature = "std")]
impl std::error::Error for CheckedClampError {}

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

    #[test]
    fn valid_clamp() {
        assert_eq!(0u8.checked_clamp(5, 15), Ok(5));
        assert_eq!(5u8.checked_clamp(5, 15), Ok(5));
        assert_eq!(10u8.checked_clamp(5, 15), Ok(10));
        assert_eq!(15u8.checked_clamp(5, 15), Ok(15));
        assert_eq!(20u8.checked_clamp(5, 15), Ok(15));

        assert_eq!(f64::NEG_INFINITY.checked_clamp(5, 15), Ok(5.0));
        assert_eq!((-100.0).checked_clamp(5, 15), Ok(5.0));
        assert_eq!(0.0.checked_clamp(5, 15), Ok(5.0));
        assert_eq!(100.0.checked_clamp(5, 15), Ok(15.0));
        assert_eq!(f64::INFINITY.checked_clamp(5, 15), Ok(15.0));
    }

    #[test]
    fn invalid_clamp() {
        use CheckedClampError as E;
        assert_eq!(0u8.checked_clamp(15, 5), Err(E::MinGreaterThanMax));
        assert_eq!(f64::NAN.checked_clamp(15, 5), Err(E::MinGreaterThanMax));
        assert_eq!(f64::NAN.checked_clamp(5, 15), Err(E::ComparisonFailed));
    }
}