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_rule2(points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
let mut violations = Vec::new();
if points.len() < 9 {
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 >= 9 {
violations.push((points[i].index, ViolationType::NineOneSide));
}
}
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
}
impl RunRule for WesternElectricRules {
fn check(&self, points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
let mut results = Vec::new();
results.extend(check_rule1(points, limits));
results.extend(check_rule2(points, limits));
results.extend(check_rule5(points, limits));
results.extend(check_rule6(points, limits));
results.sort_by_key(|&(idx, _)| idx);
results
}
}
impl RunRule for NelsonRules {
fn check(&self, points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
let mut results = Vec::new();
results.extend(check_rule1(points, limits));
results.extend(check_rule2(points, limits));
results.extend(check_rule3(points, limits));
results.extend(check_rule4(points, limits));
results.extend(check_rule5(points, limits));
results.extend(check_rule6(points, limits));
results.extend(check_rule7(points, limits));
results.extend(check_rule8(points, limits));
results.sort_by_key(|&(idx, _)| idx);
results
}
}
#[cfg(test)]
mod tests {
use super::*;
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_rule2(&points, &limits);
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_rule2(&points, &limits);
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_rule2(&points, &limits);
assert_eq!(violations.len(), 1);
}
#[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_rule2(&points, &limits);
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());
}
}