Skip to main content

dataprof_core/
quality.rs

1/// ISO 25012 quality dimensions that can be selectively requested.
2#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
3#[serde(rename_all = "lowercase")]
4pub enum QualityDimension {
5    Completeness,
6    Consistency,
7    Uniqueness,
8    Accuracy,
9    Timeliness,
10    Validity,
11    Precision,
12}
13
14impl QualityDimension {
15    /// All currently implemented dimensions.
16    pub fn all() -> Vec<Self> {
17        vec![
18            Self::Completeness,
19            Self::Consistency,
20            Self::Uniqueness,
21            Self::Accuracy,
22            Self::Timeliness,
23            Self::Validity,
24            Self::Precision,
25        ]
26    }
27}
28
29impl std::str::FromStr for QualityDimension {
30    type Err = String;
31
32    fn from_str(s: &str) -> Result<Self, Self::Err> {
33        match s.to_lowercase().as_str() {
34            "completeness" => Ok(Self::Completeness),
35            "consistency" => Ok(Self::Consistency),
36            "uniqueness" => Ok(Self::Uniqueness),
37            "accuracy" => Ok(Self::Accuracy),
38            "timeliness" => Ok(Self::Timeliness),
39            "validity" => Ok(Self::Validity),
40            "precision" => Ok(Self::Precision),
41            _ => Err(format!("Unknown quality dimension: {s}")),
42        }
43    }
44}
45
46impl std::fmt::Display for QualityDimension {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            Self::Completeness => write!(f, "completeness"),
50            Self::Consistency => write!(f, "consistency"),
51            Self::Uniqueness => write!(f, "uniqueness"),
52            Self::Accuracy => write!(f, "accuracy"),
53            Self::Timeliness => write!(f, "timeliness"),
54            Self::Validity => write!(f, "validity"),
55            Self::Precision => write!(f, "precision"),
56        }
57    }
58}
59
60/// High-level categories of analysis that can be selectively enabled.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
62#[serde(rename_all = "lowercase")]
63pub enum MetricPack {
64    /// Column names, data types, null counts — always included.
65    Schema,
66    /// Numeric stats (min/max/mean/median/std_dev/quartiles), text lengths.
67    Statistics,
68    /// Regex pattern detection (email, phone, UUID, etc.).
69    Patterns,
70    /// ISO 25012 quality dimensions (completeness, consistency, etc.).
71    Quality,
72}
73
74impl MetricPack {
75    /// All available metric packs.
76    pub fn all() -> Vec<Self> {
77        vec![
78            Self::Schema,
79            Self::Statistics,
80            Self::Patterns,
81            Self::Quality,
82        ]
83    }
84
85    /// Whether statistics should be computed given the selected packs.
86    pub fn include_statistics(packs: Option<&[Self]>) -> bool {
87        match packs {
88            None => true,
89            Some(p) => p.contains(&Self::Statistics),
90        }
91    }
92
93    /// Whether pattern detection should run given the selected packs.
94    pub fn include_patterns(packs: Option<&[Self]>) -> bool {
95        match packs {
96            None => true,
97            Some(p) => p.contains(&Self::Patterns),
98        }
99    }
100
101    /// Whether quality metrics should be computed given the selected packs.
102    pub fn include_quality(packs: Option<&[Self]>) -> bool {
103        match packs {
104            None => true,
105            Some(p) => p.contains(&Self::Quality),
106        }
107    }
108
109    /// Resolve the packs to compute, folding in an explicit quality-dimension
110    /// selection.
111    ///
112    /// Requesting an empty set of dimensions (`Some(&[])`) says "assess no
113    /// dimension", which is the same statement as not selecting the quality
114    /// pack: nothing is analyzed, so the report carries no quality. Without
115    /// this, an empty selection produces a quality object whose dimensions are
116    /// all absent — an artifact that reads like a measured result.
117    ///
118    /// This is deliberately about what was *requested*. A caller that asks for
119    /// dimensions and gets none back (an empty dataset, no date column) was
120    /// still analyzed, and keeps a quality object reporting exactly that.
121    pub fn resolve_with_dimensions(
122        packs: Option<&[Self]>,
123        dimensions: Option<&[QualityDimension]>,
124    ) -> Option<Vec<Self>> {
125        if !dimensions.is_some_and(<[QualityDimension]>::is_empty) {
126            return packs.map(<[Self]>::to_vec);
127        }
128        // `None` packs means "all packs"; make that explicit before removing
129        // Quality, so the result still selects everything else.
130        let selected = packs.map_or_else(Self::all, <[Self]>::to_vec);
131        Some(
132            selected
133                .into_iter()
134                .filter(|pack| *pack != Self::Quality)
135                .collect(),
136        )
137    }
138}
139
140impl std::str::FromStr for MetricPack {
141    type Err = String;
142
143    fn from_str(s: &str) -> Result<Self, Self::Err> {
144        match s.to_lowercase().as_str() {
145            "schema" => Ok(Self::Schema),
146            "statistics" => Ok(Self::Statistics),
147            "patterns" => Ok(Self::Patterns),
148            "quality" => Ok(Self::Quality),
149            _ => Err(format!(
150                "Unknown metric pack: {s}. Valid packs: schema, statistics, patterns, quality"
151            )),
152        }
153    }
154}
155
156impl std::fmt::Display for MetricPack {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        match self {
159            Self::Schema => write!(f, "schema"),
160            Self::Statistics => write!(f, "statistics"),
161            Self::Patterns => write!(f, "patterns"),
162            Self::Quality => write!(f, "quality"),
163        }
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_metric_pack_include_helpers_none_means_all() {
173        assert!(MetricPack::include_statistics(None));
174        assert!(MetricPack::include_patterns(None));
175        assert!(MetricPack::include_quality(None));
176    }
177
178    #[test]
179    fn test_resolve_with_dimensions_empty_selection_drops_quality() {
180        // Requesting no dimension is the same statement as not asking for
181        // quality at all, so the pack must fall out and the report carry no
182        // quality object.
183        let resolved = MetricPack::resolve_with_dimensions(None, Some(&[])).unwrap();
184        assert!(!resolved.contains(&MetricPack::Quality));
185        assert!(!MetricPack::include_quality(Some(&resolved)));
186        // The other packs survive: only quality was deselected.
187        assert!(resolved.contains(&MetricPack::Schema));
188        assert!(resolved.contains(&MetricPack::Statistics));
189        assert!(resolved.contains(&MetricPack::Patterns));
190    }
191
192    #[test]
193    fn test_resolve_with_dimensions_empty_selection_keeps_explicit_packs() {
194        let packs = vec![MetricPack::Schema, MetricPack::Quality];
195        let resolved = MetricPack::resolve_with_dimensions(Some(&packs), Some(&[])).unwrap();
196        assert_eq!(resolved, vec![MetricPack::Schema]);
197    }
198
199    #[test]
200    fn test_resolve_with_dimensions_non_empty_selection_is_untouched() {
201        // A real selection says which dimensions to assess, not whether to.
202        let dims = vec![QualityDimension::Completeness];
203        assert_eq!(MetricPack::resolve_with_dimensions(None, Some(&dims)), None);
204        assert!(MetricPack::include_quality(None));
205
206        let packs = vec![MetricPack::Quality];
207        let resolved = MetricPack::resolve_with_dimensions(Some(&packs), Some(&dims)).unwrap();
208        assert!(MetricPack::include_quality(Some(&resolved)));
209    }
210
211    #[test]
212    fn test_resolve_with_dimensions_absent_selection_is_untouched() {
213        // `None` means "all dimensions", which is not the same as "none".
214        assert_eq!(MetricPack::resolve_with_dimensions(None, None), None);
215        let packs = vec![MetricPack::Quality];
216        assert_eq!(
217            MetricPack::resolve_with_dimensions(Some(&packs), None),
218            Some(packs)
219        );
220    }
221
222    #[test]
223    fn test_metric_pack_include_helpers_selective() {
224        let packs = vec![MetricPack::Schema, MetricPack::Quality];
225        assert!(!MetricPack::include_statistics(Some(&packs)));
226        assert!(!MetricPack::include_patterns(Some(&packs)));
227        assert!(MetricPack::include_quality(Some(&packs)));
228    }
229
230    #[test]
231    fn test_metric_pack_from_str() {
232        assert_eq!(
233            "statistics".parse::<MetricPack>().unwrap(),
234            MetricPack::Statistics
235        );
236        assert_eq!(
237            "QUALITY".parse::<MetricPack>().unwrap(),
238            MetricPack::Quality
239        );
240        assert!("invalid".parse::<MetricPack>().is_err());
241    }
242}