unit-intervals 0.1.0

Constrained f32/f64 wrapper types for normalized [0, 1] and [-1, 1] values.
Documentation
use std::string::ToString;

use unit_intervals::UnitInterval;

#[test]
fn f32_constructor_accepts_unit_interval() {
    let default_width: UnitInterval = UnitInterval::new(0.5).unwrap();

    assert_eq!(default_width.get(), 0.5);
    assert_eq!(UnitInterval::<f32>::new(0.0).map(|u| u.get()), Some(0.0));
    assert_eq!(UnitInterval::<f32>::new(0.5).map(|u| u.get()), Some(0.5));
    assert_eq!(UnitInterval::<f32>::new(1.0).map(|u| u.get()), Some(1.0));
    assert_eq!(UnitInterval::<f32>::new(-0.1), None);
    assert_eq!(UnitInterval::<f32>::new(1.1), None);
}

#[test]
fn f64_constructor_accepts_unit_interval() {
    assert_eq!(UnitInterval::<f64>::new(0.0).map(|u| u.get()), Some(0.0));
    assert_eq!(UnitInterval::<f64>::new(0.5).map(|u| u.get()), Some(0.5));
    assert_eq!(UnitInterval::<f64>::new(1.0).map(|u| u.get()), Some(1.0));
    assert_eq!(UnitInterval::<f64>::new(-0.1), None);
    assert_eq!(UnitInterval::<f64>::new(1.1), None);
}

#[test]
fn constructors_reject_nan() {
    assert_eq!(UnitInterval::<f32>::new(f32::NAN), None);
    assert_eq!(UnitInterval::<f64>::new(f64::NAN), None);
}

#[test]
fn saturating_clamps_for_both_float_widths() {
    assert_eq!(UnitInterval::<f32>::saturating(-0.1).get(), 0.0);
    assert_eq!(UnitInterval::<f32>::saturating(1.1).get(), 1.0);
    assert_eq!(UnitInterval::<f32>::saturating(f32::NAN).get(), 0.0);
    assert_eq!(UnitInterval::<f64>::saturating(-0.1).get(), 0.0);
    assert_eq!(UnitInterval::<f64>::saturating(1.1).get(), 1.0);
    assert_eq!(UnitInterval::<f64>::saturating(f64::NAN).get(), 0.0);
}

#[test]
fn constants_and_basic_helpers_work() {
    assert_eq!(UnitInterval::<f32>::default(), UnitInterval::<f32>::ZERO);
    assert_eq!(UnitInterval::<f32>::ZERO.get(), 0.0);
    assert_eq!(UnitInterval::<f32>::HALF.get(), 0.5);
    assert_eq!(UnitInterval::<f32>::ONE.get(), 1.0);
    assert!(UnitInterval::<f32>::contains(0.5));
    assert!(!UnitInterval::<f32>::contains(1.5));
    assert!(UnitInterval::<f32>::ZERO.is_zero());
    assert!(UnitInterval::<f32>::ONE.is_one());
    assert_eq!(UnitInterval::new(0.25).unwrap().complement().get(), 0.75);
}

#[test]
fn standard_conversions_are_available() {
    let from_result = UnitInterval::<f32>::try_from(0.5).unwrap();
    let as_f64_interval = UnitInterval::<f64>::from(from_result);
    let as_f32_interval = UnitInterval::<f32>::from(as_f64_interval);

    assert_eq!(from_result.get(), 0.5);
    assert_eq!(
        UnitInterval::<f32>::try_from(1.5).unwrap_err().to_string(),
        "value is outside the unit interval"
    );
    assert_eq!(as_f64_interval.get(), 0.5);
    assert_eq!(as_f32_interval.get(), 0.5);
}

#[test]
fn comparison_helpers_preserve_unit_interval() {
    let low = UnitInterval::new(0.25).unwrap();
    let high = UnitInterval::new(0.75).unwrap();

    assert_eq!(low.min(high), low);
    assert_eq!(low.max(high), high);
    assert_eq!(low.midpoint(high).get(), 0.5);
    assert_eq!(low.distance_to(high).get(), 0.5);
}

#[test]
fn checked_arithmetic_rejects_out_of_range_results() {
    let low = UnitInterval::new(0.25).unwrap();
    let high = UnitInterval::new(0.75).unwrap();

    assert_eq!(low.checked_add(low).map(|u| u.get()), Some(0.5));
    assert_eq!(high.checked_add(high), None);
    assert_eq!(high.checked_sub(low).map(|u| u.get()), Some(0.5));
    assert_eq!(low.checked_sub(high), None);
    assert_eq!(low.checked_div(high).map(|u| u.get()), Some(1.0 / 3.0));
    assert_eq!(high.checked_div(low), None);
    assert_eq!(low.checked_scale(2.0).map(|u| u.get()), Some(0.5));
    assert_eq!(high.checked_scale(2.0), None);
}

#[test]
fn saturating_arithmetic_clamps_out_of_range_results() {
    let low = UnitInterval::new(0.25).unwrap();
    let high = UnitInterval::new(0.75).unwrap();

    assert_eq!(high.saturating_add(high).get(), 1.0);
    assert_eq!(low.saturating_sub(high).get(), 0.0);
    assert_eq!(high.saturating_div(low).get(), 1.0);
    assert_eq!(low.saturating_scale(-1.0).get(), 0.0);
    assert_eq!(high.saturating_scale(2.0).get(), 1.0);
}

#[test]
fn multiplication_and_lerp_are_convenient() {
    let low = UnitInterval::new(0.25).unwrap();
    let high = UnitInterval::new(0.75).unwrap();

    assert_eq!((low * high).get(), 0.1875);
    assert_eq!(UnitInterval::<f32>::HALF.lerp(10.0, 20.0), 15.0);
}

#[test]
fn unconstrained_arithmetic_returns_backing_float() {
    let low = UnitInterval::<f32>::new(0.25).unwrap();
    let high = UnitInterval::<f32>::new(0.75).unwrap();

    let sum: f32 = low + high;
    let difference: f32 = low - high;
    let product: UnitInterval<f32> = low * high;
    let scaled: f32 = low * 4.0;
    let reverse_scaled: f32 = 4.0 * low;
    let quotient: f32 = high / low;
    let remainder: f32 = high % low;
    let negated: f32 = -low;

    assert_eq!(sum, 1.0);
    assert_eq!(difference, -0.5);
    assert_eq!(product.get(), 0.1875);
    assert_eq!(scaled, 1.0);
    assert_eq!(reverse_scaled, 1.0);
    assert_eq!(quotient, 3.0);
    assert_eq!(remainder, 0.0);
    assert_eq!(negated, -0.25);
}

#[test]
fn comparisons_work_against_backing_float() {
    let half = UnitInterval::<f32>::HALF;

    assert_eq!(half, 0.5);
    assert_eq!(0.5, half);
    assert!(half < 0.75);
    assert!(0.25 < half);
    assert!(half >= 0.5);
    assert!(0.5 <= half);
}