use super::chart::{ChartPoint, ControlLimits, ViolationType};
pub trait RunRule {
fn check(&self, points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)>;
}
pub struct WesternElectricRules;
pub struct NelsonRules;
fn zone_widths(limits: &ControlLimits) -> (f64, f64) {
let sigma = (limits.ucl - limits.cl) / 3.0;
(sigma, 2.0 * sigma)
}
fn check_rule1(points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
let mut violations = Vec::new();
for point in points {
if point.value > limits.ucl || point.value < limits.lcl {
violations.push((point.index, ViolationType::BeyondLimits));
}
}
violations
}
fn check_one_side(
points: &[ChartPoint],
limits: &ControlLimits,
run: usize,
rule: ViolationType,
) -> Vec<(usize, ViolationType)> {
let mut violations = Vec::new();
if points.len() < run {
return violations;
}
let cl = limits.cl;
let sides: Vec<i8> = points
.iter()
.map(|p| {
if p.value > cl {
1
} else if p.value < cl {
-1
} else {
0
}
})
.collect();
let mut run_length = 1_usize;
for i in 1..sides.len() {
if sides[i] != 0 && sides[i] == sides[i - 1] {
run_length += 1;
} else {
run_length = 1;
}
if run_length >= run {
violations.push((points[i].index, rule));
}
}
violations
}
fn check_rule3(points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
let _ = limits; let mut violations = Vec::new();
if points.len() < 6 {
return violations;
}
let dirs: Vec<i8> = points
.windows(2)
.map(|w| {
if w[1].value > w[0].value {
1
} else if w[1].value < w[0].value {
-1
} else {
0
}
})
.collect();
let mut run_length = 1_usize;
for i in 1..dirs.len() {
if dirs[i] != 0 && dirs[i] == dirs[i - 1] {
run_length += 1;
} else {
run_length = 1;
}
if run_length >= 5 {
violations.push((points[i + 1].index, ViolationType::SixTrend));
}
}
violations
}
fn check_rule4(points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
let _ = limits;
let mut violations = Vec::new();
if points.len() < 14 {
return violations;
}
let dirs: Vec<i8> = points
.windows(2)
.map(|w| {
if w[1].value > w[0].value {
1
} else if w[1].value < w[0].value {
-1
} else {
0
}
})
.collect();
let mut alt_length = 1_usize;
for i in 1..dirs.len() {
if dirs[i] != 0 && dirs[i - 1] != 0 && dirs[i] == -dirs[i - 1] {
alt_length += 1;
} else {
alt_length = 1;
}
if alt_length >= 13 {
violations.push((points[i + 1].index, ViolationType::FourteenAlternating));
}
}
violations
}
fn check_rule5(points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
let mut violations = Vec::new();
if points.len() < 3 {
return violations;
}
let (_, two_sigma) = zone_widths(limits);
let upper_2s = limits.cl + two_sigma;
let lower_2s = limits.cl - two_sigma;
for i in 2..points.len() {
let window = &points[i - 2..=i];
let above_count = window.iter().filter(|p| p.value > upper_2s).count();
if above_count >= 2 {
violations.push((points[i].index, ViolationType::TwoOfThreeBeyond2Sigma));
continue;
}
let below_count = window.iter().filter(|p| p.value < lower_2s).count();
if below_count >= 2 {
violations.push((points[i].index, ViolationType::TwoOfThreeBeyond2Sigma));
}
}
violations
}
fn check_rule6(points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
let mut violations = Vec::new();
if points.len() < 5 {
return violations;
}
let (one_sigma, _) = zone_widths(limits);
let upper_1s = limits.cl + one_sigma;
let lower_1s = limits.cl - one_sigma;
for i in 4..points.len() {
let window = &points[i - 4..=i];
let above_count = window.iter().filter(|p| p.value > upper_1s).count();
if above_count >= 4 {
violations.push((points[i].index, ViolationType::FourOfFiveBeyond1Sigma));
continue;
}
let below_count = window.iter().filter(|p| p.value < lower_1s).count();
if below_count >= 4 {
violations.push((points[i].index, ViolationType::FourOfFiveBeyond1Sigma));
}
}
violations
}
fn check_rule7(points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
let mut violations = Vec::new();
if points.len() < 15 {
return violations;
}
let (one_sigma, _) = zone_widths(limits);
let upper_1s = limits.cl + one_sigma;
let lower_1s = limits.cl - one_sigma;
let mut run_length = 0_usize;
for point in points {
if point.value >= lower_1s && point.value <= upper_1s {
run_length += 1;
} else {
run_length = 0;
}
if run_length >= 15 {
violations.push((point.index, ViolationType::FifteenWithin1Sigma));
}
}
violations
}
fn check_rule8(points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
let mut violations = Vec::new();
if points.len() < 8 {
return violations;
}
let (one_sigma, _) = zone_widths(limits);
let upper_1s = limits.cl + one_sigma;
let lower_1s = limits.cl - one_sigma;
let mut run_length = 0_usize;
for point in points {
if point.value > upper_1s || point.value < lower_1s {
run_length += 1;
} else {
run_length = 0;
}
if run_length >= 8 {
violations.push((point.index, ViolationType::EightBeyond1Sigma));
}
}
violations
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RuleSet {
mask: u16,
}
fn bit_of(v: ViolationType) -> u16 {
let position = ViolationType::ALL
.iter()
.position(|&r| r == v)
.expect("ViolationType::ALL lists every variant");
1 << position
}
impl RuleSet {
pub const NAMES: [&'static str; 3] = ["shewhart", "western_electric", "nelson"];
pub fn shewhart() -> Self {
Self::from_iter([ViolationType::BeyondLimits])
}
pub fn nelson() -> Self {
Self::from_iter([
ViolationType::BeyondLimits,
ViolationType::NineOneSide,
ViolationType::SixTrend,
ViolationType::FourteenAlternating,
ViolationType::TwoOfThreeBeyond2Sigma,
ViolationType::FourOfFiveBeyond1Sigma,
ViolationType::FifteenWithin1Sigma,
ViolationType::EightBeyond1Sigma,
])
}
pub fn western_electric() -> Self {
Self::from_iter([
ViolationType::BeyondLimits,
ViolationType::TwoOfThreeBeyond2Sigma,
ViolationType::FourOfFiveBeyond1Sigma,
ViolationType::EightOneSide,
])
}
pub fn named(name: &str) -> Option<Self> {
match name {
"shewhart" => Some(Self::shewhart()),
"western_electric" => Some(Self::western_electric()),
"nelson" => Some(Self::nelson()),
_ => None,
}
}
pub fn none() -> Self {
Self { mask: 0 }
}
pub fn contains(&self, rule: ViolationType) -> bool {
self.mask & bit_of(rule) != 0
}
#[must_use]
pub fn with(mut self, rule: ViolationType) -> Self {
self.mask |= bit_of(rule);
self
}
#[must_use]
pub fn without(mut self, rule: ViolationType) -> Self {
self.mask &= !bit_of(rule);
self
}
pub fn iter(&self) -> impl Iterator<Item = ViolationType> + '_ {
ViolationType::ALL.into_iter().filter(|&r| self.contains(r))
}
pub fn len(&self) -> usize {
self.mask.count_ones() as usize
}
pub fn is_empty(&self) -> bool {
self.mask == 0
}
}
impl Default for RuleSet {
fn default() -> Self {
Self::shewhart()
}
}
impl FromIterator<ViolationType> for RuleSet {
fn from_iter<I: IntoIterator<Item = ViolationType>>(iter: I) -> Self {
let mut mask = 0u16;
for v in iter {
mask |= bit_of(v);
}
Self { mask }
}
}
impl RunRule for RuleSet {
fn check(&self, points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
let mut results = Vec::new();
for rule in self.iter() {
results.extend(match rule {
ViolationType::BeyondLimits => check_rule1(points, limits),
ViolationType::NineOneSide => {
check_one_side(points, limits, 9, ViolationType::NineOneSide)
}
ViolationType::EightOneSide => {
check_one_side(points, limits, 8, ViolationType::EightOneSide)
}
ViolationType::SixTrend => check_rule3(points, limits),
ViolationType::FourteenAlternating => check_rule4(points, limits),
ViolationType::TwoOfThreeBeyond2Sigma => check_rule5(points, limits),
ViolationType::FourOfFiveBeyond1Sigma => check_rule6(points, limits),
ViolationType::FifteenWithin1Sigma => check_rule7(points, limits),
ViolationType::EightBeyond1Sigma => check_rule8(points, limits),
});
}
results.sort_by_key(|&(idx, _)| idx);
results
}
}
impl RunRule for WesternElectricRules {
fn check(&self, points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
RuleSet::western_electric().check(points, limits)
}
}
impl RunRule for NelsonRules {
fn check(&self, points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
RuleSet::nelson().check(points, limits)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn nine_on_one_side() -> (Vec<ChartPoint>, ControlLimits) {
let limits = ControlLimits {
ucl: 28.0,
cl: 25.0,
lcl: 22.0,
};
let points = (0..12)
.map(|i| ChartPoint {
value: 26.0,
index: i,
violations: vec![],
})
.collect();
(points, limits)
}
#[test]
fn ruleset_applies_only_the_tests_it_holds() {
let (points, limits) = nine_on_one_side();
assert!(!RuleSet::nelson().check(&points, &limits).is_empty());
assert!(RuleSet::nelson()
.without(ViolationType::NineOneSide)
.check(&points, &limits)
.is_empty());
assert!(!RuleSet::from_iter([ViolationType::NineOneSide])
.check(&points, &limits)
.is_empty());
}
#[test]
fn ruleset_empty_reports_nothing_even_beyond_limits() {
let limits = ControlLimits {
ucl: 28.0,
cl: 25.0,
lcl: 22.0,
};
let points = vec![ChartPoint {
value: 99.0,
index: 0,
violations: vec![],
}];
assert!(!RuleSet::nelson().check(&points, &limits).is_empty());
assert!(RuleSet::none().check(&points, &limits).is_empty());
}
#[test]
fn named_sets_match_their_definitions() {
assert_eq!(RuleSet::nelson().len(), 8);
assert_eq!(RuleSet::western_electric().len(), 4);
for r in [
ViolationType::BeyondLimits,
ViolationType::TwoOfThreeBeyond2Sigma,
ViolationType::FourOfFiveBeyond1Sigma,
ViolationType::EightOneSide,
] {
assert!(RuleSet::western_electric().contains(r), "{r:?} missing");
}
for r in [
ViolationType::NineOneSide,
ViolationType::SixTrend,
ViolationType::FourteenAlternating,
ViolationType::FifteenWithin1Sigma,
ViolationType::EightBeyond1Sigma,
] {
assert!(!RuleSet::western_electric().contains(r), "{r:?} present");
}
assert!(RuleSet::none().is_empty());
}
#[test]
fn named_unit_structs_agree_with_their_rule_sets() {
let (points, limits) = nine_on_one_side();
assert_eq!(
NelsonRules.check(&points, &limits),
RuleSet::nelson().check(&points, &limits)
);
assert_eq!(
WesternElectricRules.check(&points, &limits),
RuleSet::western_electric().check(&points, &limits)
);
}
fn make_points(values: &[f64]) -> Vec<ChartPoint> {
values
.iter()
.enumerate()
.map(|(i, &v)| ChartPoint {
value: v,
index: i,
violations: Vec::new(),
})
.collect()
}
#[test]
fn test_rule1_point_above_ucl() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let points = make_points(&[25.0, 31.0, 25.0]);
let violations = check_rule1(&points, &limits);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].0, 1);
assert_eq!(violations[0].1, ViolationType::BeyondLimits);
}
#[test]
fn test_rule1_point_below_lcl() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let points = make_points(&[25.0, 19.0, 25.0]);
let violations = check_rule1(&points, &limits);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].0, 1);
}
#[test]
fn test_rule1_on_limit_is_not_violation() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let points = make_points(&[30.0, 20.0]);
let violations = check_rule1(&points, &limits);
assert!(violations.is_empty());
}
#[test]
fn test_rule2_nine_above() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let values: Vec<f64> = (0..9).map(|_| 26.0).collect();
let points = make_points(&values);
let violations = check_one_side(&points, &limits, 9, ViolationType::NineOneSide);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].1, ViolationType::NineOneSide);
}
#[test]
fn test_rule2_eight_not_enough() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let values: Vec<f64> = (0..8).map(|_| 26.0).collect();
let points = make_points(&values);
let violations = check_one_side(&points, &limits, 9, ViolationType::NineOneSide);
assert!(violations.is_empty());
}
#[test]
fn test_rule2_nine_below() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let values: Vec<f64> = (0..9).map(|_| 24.0).collect();
let points = make_points(&values);
let violations = check_one_side(&points, &limits, 9, ViolationType::NineOneSide);
assert_eq!(violations.len(), 1);
}
#[test]
fn western_electric_run_is_eight_points_nelson_is_nine() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let points = make_points(&[26.0; 8]);
let we = RuleSet::western_electric().check(&points, &limits);
assert_eq!(we, vec![(7, ViolationType::EightOneSide)]);
assert!(RuleSet::nelson().check(&points, &limits).is_empty());
assert_eq!(WesternElectricRules.check(&points, &limits), we);
let seven = make_points(&[26.0; 7]);
assert!(RuleSet::western_electric()
.check(&seven, &limits)
.is_empty());
}
#[test]
fn default_set_is_the_three_sigma_test() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let mut values = vec![26.0; 12];
values.push(31.0);
let points = make_points(&values);
assert_eq!(RuleSet::default(), RuleSet::shewhart());
assert_eq!(
RuleSet::default().check(&points, &limits),
vec![(12, ViolationType::BeyondLimits)]
);
assert!(RuleSet::nelson().check(&points, &limits).len() > 1);
}
#[test]
fn named_sets_round_trip_and_names_round_trip() {
for name in RuleSet::NAMES {
assert!(RuleSet::named(name).is_some(), "{name}");
}
assert_eq!(RuleSet::named("weco"), None);
for v in ViolationType::ALL {
assert_eq!(ViolationType::from_name(v.name()), Some(v));
}
let all: RuleSet = ViolationType::ALL.into_iter().collect();
assert_eq!(all.len(), ViolationType::ALL.len());
}
#[test]
fn test_rule3_six_increasing() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let points = make_points(&[20.0, 21.0, 22.0, 23.0, 24.0, 25.0]);
let violations = check_rule3(&points, &limits);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].1, ViolationType::SixTrend);
}
#[test]
fn test_rule3_six_decreasing() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let points = make_points(&[30.0, 29.0, 28.0, 27.0, 26.0, 25.0]);
let violations = check_rule3(&points, &limits);
assert_eq!(violations.len(), 1);
}
#[test]
fn test_rule3_five_not_enough() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let points = make_points(&[20.0, 21.0, 22.0, 23.0, 24.0]);
let violations = check_rule3(&points, &limits);
assert!(violations.is_empty());
}
#[test]
fn test_rule4_fourteen_alternating() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let values: Vec<f64> = (0..14)
.map(|i| if i % 2 == 0 { 24.0 } else { 26.0 })
.collect();
let points = make_points(&values);
let violations = check_rule4(&points, &limits);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].1, ViolationType::FourteenAlternating);
}
#[test]
fn test_rule4_thirteen_not_enough() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let values: Vec<f64> = (0..13)
.map(|i| if i % 2 == 0 { 24.0 } else { 26.0 })
.collect();
let points = make_points(&values);
let violations = check_rule4(&points, &limits);
assert!(violations.is_empty());
}
#[test]
fn test_rule5_two_of_three_above() {
let limits = ControlLimits {
ucl: 28.0, cl: 25.0,
lcl: 22.0,
};
let points = make_points(&[27.5, 25.0, 27.5]);
let violations = check_rule5(&points, &limits);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].1, ViolationType::TwoOfThreeBeyond2Sigma);
}
#[test]
fn test_rule5_two_of_three_below() {
let limits = ControlLimits {
ucl: 28.0,
cl: 25.0,
lcl: 22.0,
};
let points = make_points(&[22.5, 25.0, 22.5]);
let violations = check_rule5(&points, &limits);
assert_eq!(violations.len(), 1);
}
#[test]
fn test_rule6_four_of_five_above() {
let limits = ControlLimits {
ucl: 28.0, cl: 25.0,
lcl: 22.0,
};
let points = make_points(&[26.5, 26.5, 25.0, 26.5, 26.5]);
let violations = check_rule6(&points, &limits);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].1, ViolationType::FourOfFiveBeyond1Sigma);
}
#[test]
fn test_rule7_fifteen_within() {
let limits = ControlLimits {
ucl: 28.0, cl: 25.0,
lcl: 22.0,
};
let values: Vec<f64> = (0..15).map(|i| 24.5 + (i as f64 % 3.0) * 0.25).collect();
let points = make_points(&values);
let violations = check_rule7(&points, &limits);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].1, ViolationType::FifteenWithin1Sigma);
}
#[test]
fn test_rule8_eight_beyond() {
let limits = ControlLimits {
ucl: 28.0, cl: 25.0,
lcl: 22.0,
};
let points = make_points(&[27.0, 23.0, 27.0, 23.0, 27.0, 23.0, 27.0, 23.0]);
let violations = check_rule8(&points, &limits);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].1, ViolationType::EightBeyond1Sigma);
}
#[test]
fn test_rule8_seven_not_enough() {
let limits = ControlLimits {
ucl: 28.0,
cl: 25.0,
lcl: 22.0,
};
let points = make_points(&[27.0, 23.0, 27.0, 23.0, 27.0, 23.0, 27.0]);
let violations = check_rule8(&points, &limits);
assert!(violations.is_empty());
}
#[test]
fn test_western_electric_combines_four_rules() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let points = make_points(&[25.0, 31.0, 25.0]);
let we = WesternElectricRules;
let violations = we.check(&points, &limits);
assert!(!violations.is_empty());
assert!(violations
.iter()
.any(|(_, v)| *v == ViolationType::BeyondLimits));
}
#[test]
fn test_nelson_detects_trend() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let points = make_points(&[20.0, 21.0, 22.0, 23.0, 24.0, 25.0]);
let nelson = NelsonRules;
let violations = nelson.check(&points, &limits);
assert!(violations
.iter()
.any(|(_, v)| *v == ViolationType::SixTrend));
}
#[test]
fn test_nelson_no_violations_in_random_data() {
let limits = ControlLimits {
ucl: 28.0,
cl: 25.0,
lcl: 22.0,
};
let points = make_points(&[25.5, 24.8, 25.2, 24.9, 25.1]);
let nelson = NelsonRules;
let violations = nelson.check(&points, &limits);
assert!(violations.is_empty());
}
#[test]
fn test_nelson_rule2_continuation() {
let limits = ControlLimits {
ucl: 30.0,
cl: 25.0,
lcl: 20.0,
};
let values: Vec<f64> = (0..10).map(|_| 26.0).collect();
let points = make_points(&values);
let violations = check_one_side(&points, &limits, 9, ViolationType::NineOneSide);
assert_eq!(violations.len(), 2);
assert_eq!(violations[0].0, 8);
assert_eq!(violations[1].0, 9);
}
#[test]
fn test_rule5_not_triggered_mixed_sides() {
let limits = ControlLimits {
ucl: 28.0,
cl: 25.0,
lcl: 22.0,
};
let points = make_points(&[27.5, 25.0, 22.5]);
let violations = check_rule5(&points, &limits);
assert!(violations.is_empty());
}
#[test]
fn test_rule7_fourteen_not_enough() {
let limits = ControlLimits {
ucl: 28.0,
cl: 25.0,
lcl: 22.0,
};
let values: Vec<f64> = (0..14).map(|_| 25.5).collect();
let points = make_points(&values);
let violations = check_rule7(&points, &limits);
assert!(violations.is_empty());
}
}