1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
//! Accuracy validation and regression testing for the converter.
use crate::error::Result;
use crate::types::MunsellColor;
use super::{AccuracyStats, MunsellConverter};
impl MunsellConverter {
/// Regression test: verify the full conversion pipeline reproduces reference data.
///
/// Because reference colors are cached in a HashMap, this method will always
/// return 100% exact matches for the lookup path. Its value is as a regression
/// guard: if someone breaks the reference dataset or the HashMap, this fails.
///
/// For algorithmic accuracy measurement, use [`validate_algorithmic_accuracy`].
pub fn validate_regression(&self) -> Result<AccuracyStats> {
let mut exact_matches = 0;
let mut close_matches = 0;
let total = self.reference_data.len();
for entry in self.reference_data.iter() {
match self.srgb_to_munsell(entry.rgb) {
Ok(converted) => {
if converted.notation == entry.munsell {
exact_matches += 1;
} else if self.is_close_match(&converted.notation, &entry.munsell) {
close_matches += 1;
}
}
Err(_) => {}
}
}
Ok(AccuracyStats {
total_colors: total,
exact_matches,
close_matches,
accuracy_percentage: (exact_matches as f64 / total as f64) * 100.0,
close_match_percentage: ((exact_matches + close_matches) as f64 / total as f64) * 100.0,
})
}
/// Measure algorithmic conversion accuracy against the reference dataset.
///
/// Bypasses the HashMap lookup and tests only the mathematical conversion
/// pipeline, giving a true measurement of algorithmic accuracy.
pub fn validate_algorithmic_accuracy(&self) -> Result<AccuracyStats> {
let mut exact_matches = 0;
let mut close_matches = 0;
let total = self.reference_data.len();
for entry in self.reference_data.iter() {
match self.algorithmic_srgb_to_munsell(entry.rgb) {
Ok(converted) => {
if converted.notation == entry.munsell {
exact_matches += 1;
} else if self.is_close_match(&converted.notation, &entry.munsell) {
close_matches += 1;
}
}
Err(_) => {}
}
}
Ok(AccuracyStats {
total_colors: total,
exact_matches,
close_matches,
accuracy_percentage: (exact_matches as f64 / total as f64) * 100.0,
close_match_percentage: ((exact_matches + close_matches) as f64 / total as f64) * 100.0,
})
}
/// Validate converter accuracy against the reference dataset.
///
/// # Deprecated
/// Use [`validate_regression`] for regression testing or
/// [`validate_algorithmic_accuracy`] for true accuracy measurement.
#[deprecated(
since = "1.2.3",
note = "Use validate_regression() or validate_algorithmic_accuracy() instead."
)]
pub fn validate_accuracy(&self) -> Result<AccuracyStats> {
self.validate_regression()
}
/// Validate RGB color values.
#[inline]
pub(crate) fn validate_rgb(&self, _rgb: [u8; 3]) -> Result<()> {
// RGB values are already constrained to 0-255 by u8 type
Ok(())
}
/// Check if two Munsell notations are close matches.
pub(crate) fn is_close_match(&self, notation1: &str, notation2: &str) -> bool {
if let (Ok(color1), Ok(color2)) = (
MunsellColor::from_notation(notation1),
MunsellColor::from_notation(notation2)
) {
if color1.is_neutral() && color2.is_neutral() {
(color1.value - color2.value).abs() < 0.5
} else if let (Some(_hue1), Some(_hue2)) = (&color1.hue, &color2.hue) {
color1.hue_family() == color2.hue_family()
&& (color1.value - color2.value).abs() < 1.0
&& color1.chroma.zip(color2.chroma)
.map(|(c1, c2)| (c1 - c2).abs() < 2.0)
.unwrap_or(false)
} else {
false
}
} else {
false
}
}
}