use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub struct ControlLimits {
pub ucl: f64,
pub cl: f64,
pub lcl: f64,
}
#[derive(Debug, Clone)]
pub struct ChartPoint {
pub value: f64,
pub index: usize,
pub violations: Vec<ViolationType>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViolationType {
BeyondLimits,
NineOneSide,
SixTrend,
FourteenAlternating,
TwoOfThreeBeyond2Sigma,
FourOfFiveBeyond1Sigma,
FifteenWithin1Sigma,
EightBeyond1Sigma,
EightOneSide,
}
impl ViolationType {
pub const ALL: [ViolationType; 9] = [
ViolationType::BeyondLimits,
ViolationType::NineOneSide,
ViolationType::SixTrend,
ViolationType::FourteenAlternating,
ViolationType::TwoOfThreeBeyond2Sigma,
ViolationType::FourOfFiveBeyond1Sigma,
ViolationType::FifteenWithin1Sigma,
ViolationType::EightBeyond1Sigma,
ViolationType::EightOneSide,
];
pub fn name(self) -> &'static str {
match self {
ViolationType::BeyondLimits => "BeyondLimits",
ViolationType::NineOneSide => "NineOneSide",
ViolationType::SixTrend => "SixTrend",
ViolationType::FourteenAlternating => "FourteenAlternating",
ViolationType::TwoOfThreeBeyond2Sigma => "TwoOfThreeBeyond2Sigma",
ViolationType::FourOfFiveBeyond1Sigma => "FourOfFiveBeyond1Sigma",
ViolationType::FifteenWithin1Sigma => "FifteenWithin1Sigma",
ViolationType::EightBeyond1Sigma => "EightBeyond1Sigma",
ViolationType::EightOneSide => "EightOneSide",
}
}
pub fn from_name(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|v| v.name() == name)
}
}
#[derive(Debug, Clone)]
pub struct Violation {
pub point_index: usize,
pub violation_type: ViolationType,
}
pub trait ControlChart {
fn add_sample(&mut self, sample: &[f64]) -> Result<(), ControlChartError>;
fn control_limits(&self) -> Option<ControlLimits>;
fn is_in_control(&self) -> bool;
fn violations(&self) -> Vec<Violation>;
fn points(&self) -> &[ChartPoint];
}
pub const MIN_SUBGROUP_SIZE: usize = 2;
pub const MAX_SUBGROUP_SIZE: usize = 25;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum ControlChartError {
SampleLengthMismatch {
expected: usize,
got: usize,
},
NonFiniteValue,
DefectivesExceedSampleSize {
defectives: u64,
sample_size: u64,
},
NonPositiveUnits,
SubgroupSizeOutOfRange {
got: usize,
min: usize,
max: usize,
},
ZeroSampleSize,
InvalidStandard {
parameter: &'static str,
requirement: &'static str,
got: f64,
min: Option<f64>,
exclusive_min: Option<f64>,
max: Option<f64>,
exclusive_max: Option<f64>,
},
}
impl ControlChartError {
pub(crate) fn proportion_standard(parameter: &'static str, got: f64) -> Self {
Self::InvalidStandard {
parameter,
requirement: "in (0, 1)",
got,
min: None,
exclusive_min: Some(0.0),
max: None,
exclusive_max: Some(1.0),
}
}
pub(crate) fn rate_standard(parameter: &'static str, got: f64) -> Self {
Self::InvalidStandard {
parameter,
requirement: "> 0",
got,
min: None,
exclusive_min: Some(0.0),
max: None,
exclusive_max: None,
}
}
pub(crate) fn dispersion_standard(parameter: &'static str, got: f64) -> Self {
Self::InvalidStandard {
parameter,
requirement: ">= 0",
got,
min: Some(0.0),
exclusive_min: None,
max: None,
exclusive_max: None,
}
}
}
impl fmt::Display for ControlChartError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ControlChartError::SubgroupSizeOutOfRange { got, min, max } => {
write!(f, "subgroup size must be {min}..={max}, got {got}")
}
ControlChartError::ZeroSampleSize => write!(f, "sample size must be at least 1"),
ControlChartError::InvalidStandard {
parameter,
requirement,
got,
..
} => write!(
f,
"{parameter} must be a finite number {requirement}, got {got}"
),
ControlChartError::SampleLengthMismatch { expected, got } => {
write!(f, "sample has {got} values, expected {expected}")
}
ControlChartError::NonFiniteValue => {
write!(f, "sample contains a value that is not a finite number")
}
ControlChartError::DefectivesExceedSampleSize {
defectives,
sample_size,
} => write!(
f,
"{defectives} defectives out of a sample of {sample_size}"
),
ControlChartError::NonPositiveUnits => {
write!(f, "units inspected must be a positive, finite number")
}
}
}
}
impl std::error::Error for ControlChartError {}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum ChartInputError {
Sample {
index: usize,
error: ControlChartError,
},
TooFewSamples {
got: usize,
min: usize,
},
Standard(ControlChartError),
}
impl fmt::Display for ChartInputError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ChartInputError::Sample { index, error } => write!(f, "sample {index}: {error}"),
ChartInputError::TooFewSamples { got, min } => {
write!(f, "at least {min} samples are needed, got {got}")
}
ChartInputError::Standard(error) => error.fmt(f),
}
}
}
impl std::error::Error for ChartInputError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ChartInputError::Sample { error, .. } | ChartInputError::Standard(error) => Some(error),
ChartInputError::TooFewSamples { .. } => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_control_limits_construction() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
assert!((limits.ucl - 30.0).abs() < f64::EPSILON);
assert!((limits.cl - 25.0).abs() < f64::EPSILON);
assert!((limits.lcl - 20.0).abs() < f64::EPSILON);
}
#[test]
fn test_chart_point_construction() {
let point = ChartPoint {
value: 25.5,
index: 0,
violations: vec![ViolationType::BeyondLimits],
};
assert!((point.value - 25.5).abs() < f64::EPSILON);
assert_eq!(point.index, 0);
assert_eq!(point.violations.len(), 1);
assert_eq!(point.violations[0], ViolationType::BeyondLimits);
}
#[test]
fn test_violation_type_equality() {
assert_eq!(ViolationType::BeyondLimits, ViolationType::BeyondLimits);
assert_ne!(ViolationType::BeyondLimits, ViolationType::NineOneSide);
}
#[test]
fn test_violation_construction() {
let v = Violation {
point_index: 5,
violation_type: ViolationType::SixTrend,
};
assert_eq!(v.point_index, 5);
assert_eq!(v.violation_type, ViolationType::SixTrend);
}
#[test]
fn test_control_limits_clone() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let cloned = limits.clone();
assert_eq!(limits, cloned);
}
}