use crate::error::{DomainError, DomainErrorKind};
use stillwater::refined::{Predicate, Refined};
#[derive(Debug, Clone, Copy, Default)]
pub struct ValidPercentage;
impl Predicate<f64> for ValidPercentage {
type Error = DomainError;
fn check(value: &f64) -> Result<(), Self::Error> {
if value.is_nan() {
return Err(DomainError {
format_name: "percentage",
value: "NaN".to_string(),
reason: DomainErrorKind::InvalidFormat {
expected: "a valid number between 0 and 100",
},
example: "50.0",
});
}
if value.is_infinite() {
return Err(DomainError {
format_name: "percentage",
value: if value.is_sign_positive() {
"infinity"
} else {
"-infinity"
}
.to_string(),
reason: DomainErrorKind::InvalidFormat {
expected: "a finite number between 0 and 100",
},
example: "50.0",
});
}
if *value < 0.0 || *value > 100.0 {
return Err(DomainError {
format_name: "percentage",
value: value.to_string(),
reason: DomainErrorKind::InvalidComponent {
component: "value",
reason: format!("must be between 0 and 100, got {}", value),
},
example: "50.0",
});
}
Ok(())
}
fn description() -> &'static str {
"percentage (0 to 100)"
}
}
pub type Percentage = Refined<f64, ValidPercentage>;
pub trait PercentageExt {
fn to_decimal(&self) -> f64;
fn complement(&self) -> Percentage;
fn of(&self, value: f64) -> f64;
fn from_decimal(decimal: f64) -> Result<Percentage, DomainError>;
}
impl PercentageExt for Percentage {
fn to_decimal(&self) -> f64 {
self.get() / 100.0
}
fn complement(&self) -> Percentage {
Percentage::new(100.0 - self.get()).unwrap()
}
fn of(&self, value: f64) -> f64 {
value * self.to_decimal()
}
fn from_decimal(decimal: f64) -> Result<Percentage, DomainError> {
Percentage::new(decimal * 100.0)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ValidUnitInterval;
impl Predicate<f64> for ValidUnitInterval {
type Error = DomainError;
fn check(value: &f64) -> Result<(), Self::Error> {
if value.is_nan() {
return Err(DomainError {
format_name: "unit interval",
value: "NaN".to_string(),
reason: DomainErrorKind::InvalidFormat {
expected: "a valid number between 0 and 1",
},
example: "0.5",
});
}
if value.is_infinite() {
return Err(DomainError {
format_name: "unit interval",
value: if value.is_sign_positive() {
"infinity"
} else {
"-infinity"
}
.to_string(),
reason: DomainErrorKind::InvalidFormat {
expected: "a finite number between 0 and 1",
},
example: "0.5",
});
}
if *value < 0.0 || *value > 1.0 {
return Err(DomainError {
format_name: "unit interval",
value: value.to_string(),
reason: DomainErrorKind::InvalidComponent {
component: "value",
reason: format!("must be between 0 and 1, got {}", value),
},
example: "0.5",
});
}
Ok(())
}
fn description() -> &'static str {
"unit interval (0 to 1)"
}
}
pub type UnitInterval = Refined<f64, ValidUnitInterval>;
pub trait UnitIntervalExt {
fn to_percentage(&self) -> Percentage;
fn complement(&self) -> UnitInterval;
fn scale(&self, value: f64) -> f64;
fn from_percentage(percentage: f64) -> Result<UnitInterval, DomainError>;
}
impl UnitIntervalExt for UnitInterval {
fn to_percentage(&self) -> Percentage {
Percentage::new(self.get() * 100.0).unwrap()
}
fn complement(&self) -> UnitInterval {
UnitInterval::new(1.0 - self.get()).unwrap()
}
fn scale(&self, value: f64) -> f64 {
value * self.get()
}
fn from_percentage(percentage: f64) -> Result<UnitInterval, DomainError> {
UnitInterval::new(percentage / 100.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
mod percentage_tests {
use super::*;
#[test]
fn valid_zero() {
assert!(Percentage::new(0.0).is_ok());
}
#[test]
fn valid_hundred() {
assert!(Percentage::new(100.0).is_ok());
}
#[test]
fn valid_mid_values() {
assert!(Percentage::new(25.0).is_ok());
assert!(Percentage::new(50.0).is_ok());
assert!(Percentage::new(75.0).is_ok());
}
#[test]
fn valid_decimal_values() {
assert!(Percentage::new(33.33).is_ok());
assert!(Percentage::new(0.01).is_ok());
assert!(Percentage::new(99.99).is_ok());
}
#[test]
fn valid_negative_zero() {
let result = Percentage::new(-0.0);
assert!(result.is_ok());
assert!(*result.unwrap().get() >= 0.0);
}
#[test]
fn invalid_negative() {
let result = Percentage::new(-0.1);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.format_name, "percentage");
assert!(matches!(
err.reason,
DomainErrorKind::InvalidComponent { .. }
));
}
#[test]
fn invalid_over_hundred() {
let result = Percentage::new(100.1);
assert!(result.is_err());
}
#[test]
fn invalid_nan() {
let result = Percentage::new(f64::NAN);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.value, "NaN");
assert!(matches!(err.reason, DomainErrorKind::InvalidFormat { .. }));
}
#[test]
fn invalid_positive_infinity() {
let result = Percentage::new(f64::INFINITY);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.value, "infinity");
}
#[test]
fn invalid_negative_infinity() {
let result = Percentage::new(f64::NEG_INFINITY);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.value, "-infinity");
}
#[test]
fn ext_to_decimal() {
let pct = Percentage::new(50.0).unwrap();
assert_eq!(pct.to_decimal(), 0.5);
let zero = Percentage::new(0.0).unwrap();
assert_eq!(zero.to_decimal(), 0.0);
let hundred = Percentage::new(100.0).unwrap();
assert_eq!(hundred.to_decimal(), 1.0);
}
#[test]
fn ext_complement() {
let pct = Percentage::new(30.0).unwrap();
let comp = pct.complement();
assert_eq!(*comp.get(), 70.0);
let zero = Percentage::new(0.0).unwrap();
assert_eq!(*zero.complement().get(), 100.0);
let hundred = Percentage::new(100.0).unwrap();
assert_eq!(*hundred.complement().get(), 0.0);
}
#[test]
fn ext_of() {
let pct = Percentage::new(25.0).unwrap();
assert_eq!(pct.of(200.0), 50.0);
let ten = Percentage::new(10.0).unwrap();
assert_eq!(ten.of(50.0), 5.0);
}
#[test]
fn from_decimal_valid() {
let pct = Percentage::from_decimal(0.5).unwrap();
assert_eq!(*pct.get(), 50.0);
let zero = Percentage::from_decimal(0.0).unwrap();
assert_eq!(*zero.get(), 0.0);
let one = Percentage::from_decimal(1.0).unwrap();
assert_eq!(*one.get(), 100.0);
}
#[test]
fn from_decimal_invalid() {
assert!(Percentage::from_decimal(1.5).is_err());
assert!(Percentage::from_decimal(-0.1).is_err());
}
#[test]
fn description_returns_expected() {
assert_eq!(ValidPercentage::description(), "percentage (0 to 100)");
}
#[test]
fn error_includes_example() {
let result = Percentage::new(150.0);
let err = result.unwrap_err();
assert_eq!(err.example, "50.0");
}
}
mod unit_interval_tests {
use super::*;
#[test]
fn valid_zero() {
assert!(UnitInterval::new(0.0).is_ok());
}
#[test]
fn valid_one() {
assert!(UnitInterval::new(1.0).is_ok());
}
#[test]
fn valid_mid_values() {
assert!(UnitInterval::new(0.25).is_ok());
assert!(UnitInterval::new(0.5).is_ok());
assert!(UnitInterval::new(0.75).is_ok());
}
#[test]
fn valid_small_values() {
assert!(UnitInterval::new(0.001).is_ok());
assert!(UnitInterval::new(0.999).is_ok());
}
#[test]
fn valid_negative_zero() {
let result = UnitInterval::new(-0.0);
assert!(result.is_ok());
}
#[test]
fn invalid_negative() {
let result = UnitInterval::new(-0.01);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.format_name, "unit interval");
}
#[test]
fn invalid_over_one() {
let result = UnitInterval::new(1.01);
assert!(result.is_err());
}
#[test]
fn invalid_nan() {
let result = UnitInterval::new(f64::NAN);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.value, "NaN");
}
#[test]
fn invalid_positive_infinity() {
let result = UnitInterval::new(f64::INFINITY);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.value, "infinity");
}
#[test]
fn invalid_negative_infinity() {
let result = UnitInterval::new(f64::NEG_INFINITY);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.value, "-infinity");
}
#[test]
fn ext_to_percentage() {
let unit = UnitInterval::new(0.75).unwrap();
let pct = unit.to_percentage();
assert_eq!(*pct.get(), 75.0);
let zero = UnitInterval::new(0.0).unwrap();
assert_eq!(*zero.to_percentage().get(), 0.0);
let one = UnitInterval::new(1.0).unwrap();
assert_eq!(*one.to_percentage().get(), 100.0);
}
#[test]
fn ext_complement() {
let unit = UnitInterval::new(0.3).unwrap();
let comp = unit.complement();
assert!((comp.get() - 0.7).abs() < 0.0001);
let zero = UnitInterval::new(0.0).unwrap();
assert_eq!(*zero.complement().get(), 1.0);
let one = UnitInterval::new(1.0).unwrap();
assert_eq!(*one.complement().get(), 0.0);
}
#[test]
fn ext_scale() {
let unit = UnitInterval::new(0.5).unwrap();
assert_eq!(unit.scale(100.0), 50.0);
let opacity = UnitInterval::new(0.8).unwrap();
assert_eq!(opacity.scale(255.0), 204.0);
}
#[test]
fn from_percentage_valid() {
let unit = UnitInterval::from_percentage(75.0).unwrap();
assert_eq!(*unit.get(), 0.75);
let zero = UnitInterval::from_percentage(0.0).unwrap();
assert_eq!(*zero.get(), 0.0);
let hundred = UnitInterval::from_percentage(100.0).unwrap();
assert_eq!(*hundred.get(), 1.0);
}
#[test]
fn from_percentage_invalid() {
assert!(UnitInterval::from_percentage(150.0).is_err());
assert!(UnitInterval::from_percentage(-10.0).is_err());
}
#[test]
fn description_returns_expected() {
assert_eq!(ValidUnitInterval::description(), "unit interval (0 to 1)");
}
#[test]
fn error_includes_example() {
let result = UnitInterval::new(1.5);
let err = result.unwrap_err();
assert_eq!(err.example, "0.5");
}
}
mod conversion_tests {
use super::*;
#[test]
fn percentage_to_unit_interval_roundtrip() {
let original = 75.0;
let pct = Percentage::new(original).unwrap();
let unit = UnitInterval::new(pct.to_decimal()).unwrap();
let back = unit.to_percentage();
assert_eq!(*back.get(), original);
}
#[test]
fn unit_interval_to_percentage_roundtrip() {
let original = 0.25;
let unit = UnitInterval::new(original).unwrap();
let pct = unit.to_percentage();
let back = Percentage::from_decimal(pct.to_decimal()).unwrap();
assert!((back.get() - (original * 100.0)).abs() < 0.0001);
}
#[test]
fn from_decimal_to_decimal_roundtrip() {
let original = 0.333;
let pct = Percentage::from_decimal(original).unwrap();
let back = pct.to_decimal();
assert!((back - original).abs() < 0.0001);
}
#[test]
fn from_percentage_roundtrip() {
let original = 42.5;
let unit = UnitInterval::from_percentage(original).unwrap();
let back = unit.to_percentage();
assert!((back.get() - original).abs() < 0.0001);
}
}
mod edge_case_tests {
use super::*;
#[test]
fn boundary_values_percentage() {
assert!(Percentage::new(0.0).is_ok());
assert!(Percentage::new(50.0).is_ok());
assert!(Percentage::new(100.0).is_ok());
}
#[test]
fn just_outside_percentage_bounds() {
assert!(Percentage::new(-0.0001).is_err());
assert!(Percentage::new(100.0001).is_err());
}
#[test]
fn boundary_values_unit_interval() {
assert!(UnitInterval::new(0.0).is_ok());
assert!(UnitInterval::new(0.5).is_ok());
assert!(UnitInterval::new(1.0).is_ok());
}
#[test]
fn just_outside_unit_interval_bounds() {
assert!(UnitInterval::new(-0.0001).is_err());
assert!(UnitInterval::new(1.0001).is_err());
}
#[test]
fn very_small_valid_values() {
assert!(Percentage::new(0.0000001).is_ok());
assert!(UnitInterval::new(0.0000001).is_ok());
}
#[test]
fn complement_at_boundaries() {
let zero = Percentage::new(0.0).unwrap();
assert_eq!(*zero.complement().get(), 100.0);
let hundred = Percentage::new(100.0).unwrap();
assert_eq!(*hundred.complement().get(), 0.0);
let zero_unit = UnitInterval::new(0.0).unwrap();
assert_eq!(*zero_unit.complement().get(), 1.0);
let one_unit = UnitInterval::new(1.0).unwrap();
assert_eq!(*one_unit.complement().get(), 0.0);
}
#[test]
fn of_with_edge_values() {
let zero_pct = Percentage::new(0.0).unwrap();
assert_eq!(zero_pct.of(100.0), 0.0);
let hundred_pct = Percentage::new(100.0).unwrap();
assert_eq!(hundred_pct.of(100.0), 100.0);
let half = Percentage::new(50.0).unwrap();
assert_eq!(half.of(-100.0), -50.0);
}
#[test]
fn scale_with_edge_values() {
let zero_unit = UnitInterval::new(0.0).unwrap();
assert_eq!(zero_unit.scale(255.0), 0.0);
let one_unit = UnitInterval::new(1.0).unwrap();
assert_eq!(one_unit.scale(255.0), 255.0);
}
}
}