use crate::error::{Result, StatError};
#[derive(Debug, Clone)]
pub enum EquivalenceBounds {
Raw { lower: f64, upper: f64 },
Symmetric { delta: f64 },
CohenD { d: f64 },
}
impl EquivalenceBounds {
pub fn symmetric(delta: f64) -> Result<Self> {
if delta <= 0.0 {
return Err(StatError::InvalidParameter(
"delta must be positive".to_string(),
));
}
Ok(Self::Symmetric { delta })
}
pub fn raw(lower: f64, upper: f64) -> Result<Self> {
if lower >= upper {
return Err(StatError::InvalidParameter(format!(
"lower bound ({}) must be less than upper bound ({})",
lower, upper
)));
}
Ok(Self::Raw { lower, upper })
}
pub fn cohen_d(d: f64) -> Result<Self> {
if d <= 0.0 {
return Err(StatError::InvalidParameter(
"Cohen's d must be positive".to_string(),
));
}
Ok(Self::CohenD { d })
}
pub fn to_raw(&self, sd: Option<f64>) -> Result<(f64, f64)> {
match self {
Self::Raw { lower, upper } => Ok((*lower, *upper)),
Self::Symmetric { delta } => Ok((-*delta, *delta)),
Self::CohenD { d } => {
let sd = sd.ok_or_else(|| {
StatError::InvalidParameter(
"Standard deviation required for Cohen's d bounds".to_string(),
)
})?;
if sd <= 0.0 {
return Err(StatError::InvalidParameter(
"Standard deviation must be positive".to_string(),
));
}
let delta = d * sd;
Ok((-delta, delta))
}
}
}
pub fn validate(&self) -> Result<()> {
match self {
Self::Raw { lower, upper } => {
if *lower >= *upper {
return Err(StatError::InvalidParameter(format!(
"lower bound ({}) must be less than upper bound ({})",
lower, upper
)));
}
}
Self::Symmetric { delta } => {
if *delta <= 0.0 {
return Err(StatError::InvalidParameter(
"delta must be positive".to_string(),
));
}
}
Self::CohenD { d } => {
if *d <= 0.0 {
return Err(StatError::InvalidParameter(
"Cohen's d must be positive".to_string(),
));
}
}
}
Ok(())
}
}
impl Default for EquivalenceBounds {
fn default() -> Self {
Self::CohenD { d: 0.5 }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_symmetric_bounds() {
let bounds = EquivalenceBounds::symmetric(0.5).unwrap();
let (lower, upper) = bounds.to_raw(None).unwrap();
assert!((lower - (-0.5)).abs() < 1e-10);
assert!((upper - 0.5).abs() < 1e-10);
}
#[test]
fn test_raw_bounds() {
let bounds = EquivalenceBounds::raw(-0.3, 0.7).unwrap();
let (lower, upper) = bounds.to_raw(None).unwrap();
assert!((lower - (-0.3)).abs() < 1e-10);
assert!((upper - 0.7).abs() < 1e-10);
}
#[test]
fn test_cohen_d_bounds() {
let bounds = EquivalenceBounds::cohen_d(0.5).unwrap();
let (lower, upper) = bounds.to_raw(Some(2.0)).unwrap();
assert!((lower - (-1.0)).abs() < 1e-10);
assert!((upper - 1.0).abs() < 1e-10);
}
#[test]
fn test_invalid_bounds() {
assert!(EquivalenceBounds::symmetric(-0.5).is_err());
assert!(EquivalenceBounds::raw(0.5, -0.5).is_err());
assert!(EquivalenceBounds::cohen_d(0.0).is_err());
}
#[test]
fn test_cohen_d_requires_sd() {
let bounds = EquivalenceBounds::cohen_d(0.5).unwrap();
assert!(bounds.to_raw(None).is_err());
}
}