use nutype::nutype;
#[nutype(
validate(finite, greater_or_equal = 0.0, less_or_equal = 1.0),
derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)
)]
pub struct MetricValue(f64);
impl MetricValue {
pub fn perfect() -> Self {
Self::try_new(1.0).unwrap()
}
pub fn zero() -> Self {
Self::try_new(0.0).unwrap()
}
pub fn high_quality() -> Self {
Self::try_new(0.8).unwrap()
}
pub fn medium_quality() -> Self {
Self::try_new(0.6).unwrap()
}
pub fn low_quality() -> Self {
Self::try_new(0.4).unwrap()
}
pub fn percentage_change_from(&self, other: MetricValue) -> Option<PercentageChange> {
if other.into_inner() == 0.0 {
return None;
}
let change = (self.into_inner() - other.into_inner()) / other.into_inner();
PercentageChange::try_new(change).ok()
}
}
#[nutype(
validate(finite, greater = 0.0, less_or_equal = 0.5), // Max 50% threshold
derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)
)]
pub struct StabilityThreshold(f64);
impl StabilityThreshold {
pub fn very_sensitive() -> Self {
Self::try_new(0.01).unwrap()
}
pub fn standard() -> Self {
Self::try_new(0.02).unwrap()
}
pub fn conservative() -> Self {
Self::try_new(0.05).unwrap()
}
pub fn relaxed() -> Self {
Self::try_new(0.1).unwrap()
}
pub fn is_significant_change(&self, change: f64) -> bool {
change.abs() > self.into_inner()
}
}
#[nutype(
validate(finite, greater_or_equal = -1.0), // -100% minimum (complete loss)
derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)
)]
pub struct PercentageChange(f64);
impl PercentageChange {
pub fn none() -> Self {
Self::try_new(0.0).unwrap()
}
pub fn small_improvement() -> Self {
Self::try_new(0.05).unwrap()
}
pub fn large_improvement() -> Self {
Self::try_new(0.25).unwrap()
}
pub fn small_decline() -> Self {
Self::try_new(-0.05).unwrap()
}
pub fn large_decline() -> Self {
Self::try_new(-0.25).unwrap()
}
pub fn complete_loss() -> Self {
Self::try_new(-1.0).unwrap()
}
pub fn as_percentage_points(&self) -> f64 {
self.into_inner() * 100.0
}
pub fn is_improvement(&self) -> bool {
self.into_inner() > 0.0
}
pub fn is_decline(&self) -> bool {
self.into_inner() < 0.0
}
pub fn is_stable(&self, threshold: StabilityThreshold) -> bool {
threshold.is_significant_change(self.into_inner())
}
}
impl Default for StabilityThreshold {
fn default() -> Self {
Self::try_new(0.02).unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metric_value_validation() {
assert!(MetricValue::try_new(0.0).is_ok());
assert!(MetricValue::try_new(0.5).is_ok());
assert!(MetricValue::try_new(1.0).is_ok());
assert!(MetricValue::try_new(-0.1).is_err());
assert!(MetricValue::try_new(1.1).is_err());
}
#[test]
fn test_stability_threshold_validation() {
assert!(StabilityThreshold::try_new(0.01).is_ok());
assert!(StabilityThreshold::try_new(0.1).is_ok());
assert!(StabilityThreshold::try_new(0.5).is_ok());
assert!(StabilityThreshold::try_new(0.0).is_err()); assert!(StabilityThreshold::try_new(0.6).is_err()); }
#[test]
fn test_percentage_change_calculation() {
let current = MetricValue::try_new(0.9).unwrap();
let previous = MetricValue::try_new(0.8).unwrap();
let change = current.percentage_change_from(previous).unwrap();
assert!((change.as_percentage_points() - 12.5).abs() < 1e-10); assert!(change.is_improvement());
}
#[test]
fn test_significance_checking() {
let threshold = StabilityThreshold::default();
assert!(threshold.is_significant_change(0.05)); assert!(!threshold.is_significant_change(0.01)); }
#[test]
fn test_semantic_constructors() {
assert_eq!(MetricValue::perfect().into_inner(), 1.0);
assert_eq!(MetricValue::zero().into_inner(), 0.0);
assert_eq!(StabilityThreshold::default().into_inner(), 0.02);
assert_eq!(PercentageChange::none().into_inner(), 0.0);
}
}