1use super::chart::{ChartPoint, ControlLimits, ViolationType};
14
15pub trait RunRule {
20 fn check(&self, points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)>;
25}
26
27pub struct WesternElectricRules;
37
38pub struct NelsonRules;
59
60fn zone_widths(limits: &ControlLimits) -> (f64, f64) {
68 let sigma = (limits.ucl - limits.cl) / 3.0;
69 (sigma, 2.0 * sigma)
70}
71
72fn check_rule1(points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
76 let mut violations = Vec::new();
77 for point in points {
78 if point.value > limits.ucl || point.value < limits.lcl {
79 violations.push((point.index, ViolationType::BeyondLimits));
80 }
81 }
82 violations
83}
84
85fn check_rule2(points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
89 let mut violations = Vec::new();
90 if points.len() < 9 {
91 return violations;
92 }
93
94 let cl = limits.cl;
95 let sides: Vec<i8> = points
98 .iter()
99 .map(|p| {
100 if p.value > cl {
101 1
102 } else if p.value < cl {
103 -1
104 } else {
105 0
106 }
107 })
108 .collect();
109
110 let mut run_length = 1_usize;
111 for i in 1..sides.len() {
112 if sides[i] != 0 && sides[i] == sides[i - 1] {
113 run_length += 1;
114 } else {
115 run_length = 1;
116 }
117 if run_length >= 9 {
118 violations.push((points[i].index, ViolationType::NineOneSide));
119 }
120 }
121 violations
122}
123
124fn check_rule3(points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
128 let _ = limits; let mut violations = Vec::new();
130 if points.len() < 6 {
131 return violations;
132 }
133
134 let dirs: Vec<i8> = points
136 .windows(2)
137 .map(|w| {
138 if w[1].value > w[0].value {
139 1
140 } else if w[1].value < w[0].value {
141 -1
142 } else {
143 0
144 }
145 })
146 .collect();
147
148 let mut run_length = 1_usize;
149 for i in 1..dirs.len() {
150 if dirs[i] != 0 && dirs[i] == dirs[i - 1] {
151 run_length += 1;
152 } else {
153 run_length = 1;
154 }
155 if run_length >= 5 {
158 violations.push((points[i + 1].index, ViolationType::SixTrend));
161 }
162 }
163 violations
164}
165
166fn check_rule4(points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
170 let _ = limits;
171 let mut violations = Vec::new();
172 if points.len() < 14 {
173 return violations;
174 }
175
176 let dirs: Vec<i8> = points
178 .windows(2)
179 .map(|w| {
180 if w[1].value > w[0].value {
181 1
182 } else if w[1].value < w[0].value {
183 -1
184 } else {
185 0
186 }
187 })
188 .collect();
189
190 let mut alt_length = 1_usize;
192 for i in 1..dirs.len() {
193 if dirs[i] != 0 && dirs[i - 1] != 0 && dirs[i] == -dirs[i - 1] {
194 alt_length += 1;
195 } else {
196 alt_length = 1;
197 }
198 if alt_length >= 13 {
202 violations.push((points[i + 1].index, ViolationType::FourteenAlternating));
203 }
204 }
205 violations
206}
207
208fn check_rule5(points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
212 let mut violations = Vec::new();
213 if points.len() < 3 {
214 return violations;
215 }
216
217 let (_, two_sigma) = zone_widths(limits);
218 let upper_2s = limits.cl + two_sigma;
219 let lower_2s = limits.cl - two_sigma;
220
221 for i in 2..points.len() {
222 let window = &points[i - 2..=i];
223
224 let above_count = window.iter().filter(|p| p.value > upper_2s).count();
226 if above_count >= 2 {
227 violations.push((points[i].index, ViolationType::TwoOfThreeBeyond2Sigma));
228 continue;
229 }
230
231 let below_count = window.iter().filter(|p| p.value < lower_2s).count();
233 if below_count >= 2 {
234 violations.push((points[i].index, ViolationType::TwoOfThreeBeyond2Sigma));
235 }
236 }
237 violations
238}
239
240fn check_rule6(points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
244 let mut violations = Vec::new();
245 if points.len() < 5 {
246 return violations;
247 }
248
249 let (one_sigma, _) = zone_widths(limits);
250 let upper_1s = limits.cl + one_sigma;
251 let lower_1s = limits.cl - one_sigma;
252
253 for i in 4..points.len() {
254 let window = &points[i - 4..=i];
255
256 let above_count = window.iter().filter(|p| p.value > upper_1s).count();
258 if above_count >= 4 {
259 violations.push((points[i].index, ViolationType::FourOfFiveBeyond1Sigma));
260 continue;
261 }
262
263 let below_count = window.iter().filter(|p| p.value < lower_1s).count();
265 if below_count >= 4 {
266 violations.push((points[i].index, ViolationType::FourOfFiveBeyond1Sigma));
267 }
268 }
269 violations
270}
271
272fn check_rule7(points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
276 let mut violations = Vec::new();
277 if points.len() < 15 {
278 return violations;
279 }
280
281 let (one_sigma, _) = zone_widths(limits);
282 let upper_1s = limits.cl + one_sigma;
283 let lower_1s = limits.cl - one_sigma;
284
285 let mut run_length = 0_usize;
286 for point in points {
287 if point.value >= lower_1s && point.value <= upper_1s {
288 run_length += 1;
289 } else {
290 run_length = 0;
291 }
292 if run_length >= 15 {
293 violations.push((point.index, ViolationType::FifteenWithin1Sigma));
294 }
295 }
296 violations
297}
298
299fn check_rule8(points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
303 let mut violations = Vec::new();
304 if points.len() < 8 {
305 return violations;
306 }
307
308 let (one_sigma, _) = zone_widths(limits);
309 let upper_1s = limits.cl + one_sigma;
310 let lower_1s = limits.cl - one_sigma;
311
312 let mut run_length = 0_usize;
313 for point in points {
314 if point.value > upper_1s || point.value < lower_1s {
316 run_length += 1;
317 } else {
318 run_length = 0;
319 }
320 if run_length >= 8 {
321 violations.push((point.index, ViolationType::EightBeyond1Sigma));
322 }
323 }
324 violations
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq)]
363pub struct RuleSet {
364 mask: u8,
365}
366
367const ALL_RULES: [ViolationType; 8] = [
369 ViolationType::BeyondLimits,
370 ViolationType::NineOneSide,
371 ViolationType::SixTrend,
372 ViolationType::FourteenAlternating,
373 ViolationType::TwoOfThreeBeyond2Sigma,
374 ViolationType::FourOfFiveBeyond1Sigma,
375 ViolationType::FifteenWithin1Sigma,
376 ViolationType::EightBeyond1Sigma,
377];
378
379fn bit_of(v: ViolationType) -> u8 {
380 match v {
381 ViolationType::BeyondLimits => 1 << 0,
382 ViolationType::NineOneSide => 1 << 1,
383 ViolationType::SixTrend => 1 << 2,
384 ViolationType::FourteenAlternating => 1 << 3,
385 ViolationType::TwoOfThreeBeyond2Sigma => 1 << 4,
386 ViolationType::FourOfFiveBeyond1Sigma => 1 << 5,
387 ViolationType::FifteenWithin1Sigma => 1 << 6,
388 ViolationType::EightBeyond1Sigma => 1 << 7,
389 }
390}
391
392impl RuleSet {
393 pub fn nelson() -> Self {
396 Self { mask: u8::MAX }
397 }
398
399 pub fn western_electric() -> Self {
401 Self::from_iter([
402 ViolationType::BeyondLimits,
403 ViolationType::NineOneSide,
404 ViolationType::TwoOfThreeBeyond2Sigma,
405 ViolationType::FourOfFiveBeyond1Sigma,
406 ])
407 }
408
409 pub fn none() -> Self {
413 Self { mask: 0 }
414 }
415
416 pub fn contains(&self, rule: ViolationType) -> bool {
418 self.mask & bit_of(rule) != 0
419 }
420
421 #[must_use]
423 pub fn with(mut self, rule: ViolationType) -> Self {
424 self.mask |= bit_of(rule);
425 self
426 }
427
428 #[must_use]
430 pub fn without(mut self, rule: ViolationType) -> Self {
431 self.mask &= !bit_of(rule);
432 self
433 }
434
435 pub fn iter(&self) -> impl Iterator<Item = ViolationType> + '_ {
437 ALL_RULES.into_iter().filter(|&r| self.contains(r))
438 }
439
440 pub fn len(&self) -> usize {
442 self.mask.count_ones() as usize
443 }
444
445 pub fn is_empty(&self) -> bool {
447 self.mask == 0
448 }
449}
450
451impl Default for RuleSet {
452 fn default() -> Self {
455 Self::nelson()
456 }
457}
458
459impl FromIterator<ViolationType> for RuleSet {
460 fn from_iter<I: IntoIterator<Item = ViolationType>>(iter: I) -> Self {
461 let mut mask = 0u8;
462 for v in iter {
463 mask |= bit_of(v);
464 }
465 Self { mask }
466 }
467}
468
469impl RunRule for RuleSet {
470 fn check(&self, points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
471 let mut results = Vec::new();
472 for rule in self.iter() {
473 results.extend(match rule {
474 ViolationType::BeyondLimits => check_rule1(points, limits),
475 ViolationType::NineOneSide => check_rule2(points, limits),
476 ViolationType::SixTrend => check_rule3(points, limits),
477 ViolationType::FourteenAlternating => check_rule4(points, limits),
478 ViolationType::TwoOfThreeBeyond2Sigma => check_rule5(points, limits),
479 ViolationType::FourOfFiveBeyond1Sigma => check_rule6(points, limits),
480 ViolationType::FifteenWithin1Sigma => check_rule7(points, limits),
481 ViolationType::EightBeyond1Sigma => check_rule8(points, limits),
482 });
483 }
484 results.sort_by_key(|&(idx, _)| idx);
485 results
486 }
487}
488
489impl RunRule for WesternElectricRules {
494 fn check(&self, points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
498 RuleSet::western_electric().check(points, limits)
499 }
500}
501
502impl RunRule for NelsonRules {
503 fn check(&self, points: &[ChartPoint], limits: &ControlLimits) -> Vec<(usize, ViolationType)> {
505 RuleSet::nelson().check(points, limits)
506 }
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512
513 fn nine_on_one_side() -> (Vec<ChartPoint>, ControlLimits) {
518 let limits = ControlLimits {
519 ucl: 28.0,
520 cl: 25.0,
521 lcl: 22.0,
522 };
523 let points = (0..12)
524 .map(|i| ChartPoint {
525 value: 26.0,
526 index: i,
527 violations: vec![],
528 })
529 .collect();
530 (points, limits)
531 }
532
533 #[test]
534 fn ruleset_applies_only_the_tests_it_holds() {
535 let (points, limits) = nine_on_one_side();
536
537 assert!(!RuleSet::nelson().check(&points, &limits).is_empty());
539 assert!(RuleSet::nelson()
540 .without(ViolationType::NineOneSide)
541 .check(&points, &limits)
542 .is_empty());
543
544 assert!(!RuleSet::from_iter([ViolationType::NineOneSide])
547 .check(&points, &limits)
548 .is_empty());
549 }
550
551 #[test]
552 fn ruleset_empty_reports_nothing_even_beyond_limits() {
553 let limits = ControlLimits {
554 ucl: 28.0,
555 cl: 25.0,
556 lcl: 22.0,
557 };
558 let points = vec![ChartPoint {
559 value: 99.0,
560 index: 0,
561 violations: vec![],
562 }];
563 assert!(!RuleSet::nelson().check(&points, &limits).is_empty());
564 assert!(RuleSet::none().check(&points, &limits).is_empty());
565 }
566
567 #[test]
568 fn named_sets_match_their_definitions() {
569 assert_eq!(RuleSet::nelson().len(), 8);
570 assert_eq!(RuleSet::western_electric().len(), 4);
571 for r in [
572 ViolationType::BeyondLimits,
573 ViolationType::NineOneSide,
574 ViolationType::TwoOfThreeBeyond2Sigma,
575 ViolationType::FourOfFiveBeyond1Sigma,
576 ] {
577 assert!(RuleSet::western_electric().contains(r), "{r:?} missing");
578 }
579 for r in [
580 ViolationType::SixTrend,
581 ViolationType::FourteenAlternating,
582 ViolationType::FifteenWithin1Sigma,
583 ViolationType::EightBeyond1Sigma,
584 ] {
585 assert!(!RuleSet::western_electric().contains(r), "{r:?} present");
586 }
587 assert!(RuleSet::none().is_empty());
588 }
589
590 #[test]
593 fn named_unit_structs_agree_with_their_rule_sets() {
594 let (points, limits) = nine_on_one_side();
595 assert_eq!(
596 NelsonRules.check(&points, &limits),
597 RuleSet::nelson().check(&points, &limits)
598 );
599 assert_eq!(
600 WesternElectricRules.check(&points, &limits),
601 RuleSet::western_electric().check(&points, &limits)
602 );
603 }
604
605 fn make_points(values: &[f64]) -> Vec<ChartPoint> {
607 values
608 .iter()
609 .enumerate()
610 .map(|(i, &v)| ChartPoint {
611 value: v,
612 index: i,
613 violations: Vec::new(),
614 })
615 .collect()
616 }
617
618 #[test]
621 fn test_rule1_point_above_ucl() {
622 let limits = ControlLimits {
623 ucl: 30.0,
624 cl: 25.0,
625 lcl: 20.0,
626 };
627 let points = make_points(&[25.0, 31.0, 25.0]);
628 let violations = check_rule1(&points, &limits);
629 assert_eq!(violations.len(), 1);
630 assert_eq!(violations[0].0, 1);
631 assert_eq!(violations[0].1, ViolationType::BeyondLimits);
632 }
633
634 #[test]
635 fn test_rule1_point_below_lcl() {
636 let limits = ControlLimits {
637 ucl: 30.0,
638 cl: 25.0,
639 lcl: 20.0,
640 };
641 let points = make_points(&[25.0, 19.0, 25.0]);
642 let violations = check_rule1(&points, &limits);
643 assert_eq!(violations.len(), 1);
644 assert_eq!(violations[0].0, 1);
645 }
646
647 #[test]
648 fn test_rule1_on_limit_is_not_violation() {
649 let limits = ControlLimits {
650 ucl: 30.0,
651 cl: 25.0,
652 lcl: 20.0,
653 };
654 let points = make_points(&[30.0, 20.0]);
655 let violations = check_rule1(&points, &limits);
656 assert!(violations.is_empty());
657 }
658
659 #[test]
662 fn test_rule2_nine_above() {
663 let limits = ControlLimits {
664 ucl: 30.0,
665 cl: 25.0,
666 lcl: 20.0,
667 };
668 let values: Vec<f64> = (0..9).map(|_| 26.0).collect();
670 let points = make_points(&values);
671 let violations = check_rule2(&points, &limits);
672 assert_eq!(violations.len(), 1);
673 assert_eq!(violations[0].1, ViolationType::NineOneSide);
674 }
675
676 #[test]
677 fn test_rule2_eight_not_enough() {
678 let limits = ControlLimits {
679 ucl: 30.0,
680 cl: 25.0,
681 lcl: 20.0,
682 };
683 let values: Vec<f64> = (0..8).map(|_| 26.0).collect();
684 let points = make_points(&values);
685 let violations = check_rule2(&points, &limits);
686 assert!(violations.is_empty());
687 }
688
689 #[test]
690 fn test_rule2_nine_below() {
691 let limits = ControlLimits {
692 ucl: 30.0,
693 cl: 25.0,
694 lcl: 20.0,
695 };
696 let values: Vec<f64> = (0..9).map(|_| 24.0).collect();
697 let points = make_points(&values);
698 let violations = check_rule2(&points, &limits);
699 assert_eq!(violations.len(), 1);
700 }
701
702 #[test]
705 fn test_rule3_six_increasing() {
706 let limits = ControlLimits {
707 ucl: 30.0,
708 cl: 25.0,
709 lcl: 20.0,
710 };
711 let points = make_points(&[20.0, 21.0, 22.0, 23.0, 24.0, 25.0]);
712 let violations = check_rule3(&points, &limits);
713 assert_eq!(violations.len(), 1);
714 assert_eq!(violations[0].1, ViolationType::SixTrend);
715 }
716
717 #[test]
718 fn test_rule3_six_decreasing() {
719 let limits = ControlLimits {
720 ucl: 30.0,
721 cl: 25.0,
722 lcl: 20.0,
723 };
724 let points = make_points(&[30.0, 29.0, 28.0, 27.0, 26.0, 25.0]);
725 let violations = check_rule3(&points, &limits);
726 assert_eq!(violations.len(), 1);
727 }
728
729 #[test]
730 fn test_rule3_five_not_enough() {
731 let limits = ControlLimits {
732 ucl: 30.0,
733 cl: 25.0,
734 lcl: 20.0,
735 };
736 let points = make_points(&[20.0, 21.0, 22.0, 23.0, 24.0]);
737 let violations = check_rule3(&points, &limits);
738 assert!(violations.is_empty());
739 }
740
741 #[test]
744 fn test_rule4_fourteen_alternating() {
745 let limits = ControlLimits {
746 ucl: 30.0,
747 cl: 25.0,
748 lcl: 20.0,
749 };
750 let values: Vec<f64> = (0..14)
752 .map(|i| if i % 2 == 0 { 24.0 } else { 26.0 })
753 .collect();
754 let points = make_points(&values);
755 let violations = check_rule4(&points, &limits);
756 assert_eq!(violations.len(), 1);
757 assert_eq!(violations[0].1, ViolationType::FourteenAlternating);
758 }
759
760 #[test]
761 fn test_rule4_thirteen_not_enough() {
762 let limits = ControlLimits {
763 ucl: 30.0,
764 cl: 25.0,
765 lcl: 20.0,
766 };
767 let values: Vec<f64> = (0..13)
768 .map(|i| if i % 2 == 0 { 24.0 } else { 26.0 })
769 .collect();
770 let points = make_points(&values);
771 let violations = check_rule4(&points, &limits);
772 assert!(violations.is_empty());
773 }
774
775 #[test]
778 fn test_rule5_two_of_three_above() {
779 let limits = ControlLimits {
780 ucl: 28.0, cl: 25.0,
782 lcl: 22.0,
783 };
784 let points = make_points(&[27.5, 25.0, 27.5]);
787 let violations = check_rule5(&points, &limits);
788 assert_eq!(violations.len(), 1);
789 assert_eq!(violations[0].1, ViolationType::TwoOfThreeBeyond2Sigma);
790 }
791
792 #[test]
793 fn test_rule5_two_of_three_below() {
794 let limits = ControlLimits {
795 ucl: 28.0,
796 cl: 25.0,
797 lcl: 22.0,
798 };
799 let points = make_points(&[22.5, 25.0, 22.5]);
801 let violations = check_rule5(&points, &limits);
802 assert_eq!(violations.len(), 1);
803 }
804
805 #[test]
808 fn test_rule6_four_of_five_above() {
809 let limits = ControlLimits {
810 ucl: 28.0, cl: 25.0,
812 lcl: 22.0,
813 };
814 let points = make_points(&[26.5, 26.5, 25.0, 26.5, 26.5]);
816 let violations = check_rule6(&points, &limits);
817 assert_eq!(violations.len(), 1);
818 assert_eq!(violations[0].1, ViolationType::FourOfFiveBeyond1Sigma);
819 }
820
821 #[test]
824 fn test_rule7_fifteen_within() {
825 let limits = ControlLimits {
826 ucl: 28.0, cl: 25.0,
828 lcl: 22.0,
829 };
830 let values: Vec<f64> = (0..15).map(|i| 24.5 + (i as f64 % 3.0) * 0.25).collect();
832 let points = make_points(&values);
833 let violations = check_rule7(&points, &limits);
834 assert_eq!(violations.len(), 1);
835 assert_eq!(violations[0].1, ViolationType::FifteenWithin1Sigma);
836 }
837
838 #[test]
841 fn test_rule8_eight_beyond() {
842 let limits = ControlLimits {
843 ucl: 28.0, cl: 25.0,
845 lcl: 22.0,
846 };
847 let points = make_points(&[27.0, 23.0, 27.0, 23.0, 27.0, 23.0, 27.0, 23.0]);
849 let violations = check_rule8(&points, &limits);
850 assert_eq!(violations.len(), 1);
851 assert_eq!(violations[0].1, ViolationType::EightBeyond1Sigma);
852 }
853
854 #[test]
855 fn test_rule8_seven_not_enough() {
856 let limits = ControlLimits {
857 ucl: 28.0,
858 cl: 25.0,
859 lcl: 22.0,
860 };
861 let points = make_points(&[27.0, 23.0, 27.0, 23.0, 27.0, 23.0, 27.0]);
862 let violations = check_rule8(&points, &limits);
863 assert!(violations.is_empty());
864 }
865
866 #[test]
869 fn test_western_electric_combines_four_rules() {
870 let limits = ControlLimits {
871 ucl: 30.0,
872 cl: 25.0,
873 lcl: 20.0,
874 };
875 let points = make_points(&[25.0, 31.0, 25.0]);
877 let we = WesternElectricRules;
878 let violations = we.check(&points, &limits);
879 assert!(!violations.is_empty());
880 assert!(violations
881 .iter()
882 .any(|(_, v)| *v == ViolationType::BeyondLimits));
883 }
884
885 #[test]
888 fn test_nelson_detects_trend() {
889 let limits = ControlLimits {
890 ucl: 30.0,
891 cl: 25.0,
892 lcl: 20.0,
893 };
894 let points = make_points(&[20.0, 21.0, 22.0, 23.0, 24.0, 25.0]);
895 let nelson = NelsonRules;
896 let violations = nelson.check(&points, &limits);
897 assert!(violations
898 .iter()
899 .any(|(_, v)| *v == ViolationType::SixTrend));
900 }
901
902 #[test]
903 fn test_nelson_no_violations_in_random_data() {
904 let limits = ControlLimits {
905 ucl: 28.0,
906 cl: 25.0,
907 lcl: 22.0,
908 };
909 let points = make_points(&[25.5, 24.8, 25.2, 24.9, 25.1]);
911 let nelson = NelsonRules;
912 let violations = nelson.check(&points, &limits);
913 assert!(violations.is_empty());
914 }
915
916 #[test]
917 fn test_nelson_rule2_continuation() {
918 let limits = ControlLimits {
920 ucl: 30.0,
921 cl: 25.0,
922 lcl: 20.0,
923 };
924 let values: Vec<f64> = (0..10).map(|_| 26.0).collect();
925 let points = make_points(&values);
926 let violations = check_rule2(&points, &limits);
927 assert_eq!(violations.len(), 2);
929 assert_eq!(violations[0].0, 8);
930 assert_eq!(violations[1].0, 9);
931 }
932
933 #[test]
934 fn test_rule5_not_triggered_mixed_sides() {
935 let limits = ControlLimits {
936 ucl: 28.0,
937 cl: 25.0,
938 lcl: 22.0,
939 };
940 let points = make_points(&[27.5, 25.0, 22.5]);
943 let violations = check_rule5(&points, &limits);
944 assert!(violations.is_empty());
945 }
946
947 #[test]
948 fn test_rule7_fourteen_not_enough() {
949 let limits = ControlLimits {
950 ucl: 28.0,
951 cl: 25.0,
952 lcl: 22.0,
953 };
954 let values: Vec<f64> = (0..14).map(|_| 25.5).collect();
955 let points = make_points(&values);
956 let violations = check_rule7(&points, &limits);
957 assert!(violations.is_empty());
958 }
959}