1#[derive(
3 Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
4)]
5pub enum DataType {
6 String,
8 Identifier,
10 Integer,
12 Float,
14 Date,
16 Boolean,
18 Nested,
24}
25
26impl DataType {
27 pub fn name(&self) -> &'static str {
31 match self {
32 Self::String => "string",
33 Self::Identifier => "identifier",
34 Self::Integer => "integer",
35 Self::Float => "float",
36 Self::Date => "date",
37 Self::Boolean => "boolean",
38 Self::Nested => "nested",
39 }
40 }
41
42 pub fn from_name(name: &str) -> Option<Self> {
45 Some(match name {
46 "string" => Self::String,
47 "identifier" => Self::Identifier,
48 "integer" => Self::Integer,
49 "float" => Self::Float,
50 "date" => Self::Date,
51 "boolean" => Self::Boolean,
52 "nested" => Self::Nested,
53 _ => return None,
54 })
55 }
56}
57
58#[derive(
66 Debug,
67 Clone,
68 Copy,
69 PartialEq,
70 Eq,
71 Hash,
72 serde::Serialize,
73 serde::Deserialize,
74 schemars::JsonSchema,
75)]
76#[serde(rename_all = "lowercase")]
77pub enum LexicalClass {
78 Numeric,
81 Date,
83 Boolean,
85 Text,
87}
88
89impl std::fmt::Display for LexicalClass {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 match self {
92 Self::Numeric => write!(f, "numeric"),
93 Self::Date => write!(f, "date"),
94 Self::Boolean => write!(f, "boolean"),
95 Self::Text => write!(f, "text"),
96 }
97 }
98}
99
100#[derive(
111 Debug,
112 Clone,
113 Copy,
114 Default,
115 PartialEq,
116 Eq,
117 serde::Serialize,
118 serde::Deserialize,
119 schemars::JsonSchema,
120)]
121pub struct TypeHomogeneity {
122 pub numeric: usize,
123 pub date: usize,
124 pub boolean: usize,
125 pub text: usize,
126}
127
128impl TypeHomogeneity {
129 fn counts(&self) -> [(LexicalClass, usize); 4] {
131 [
132 (LexicalClass::Numeric, self.numeric),
133 (LexicalClass::Date, self.date),
134 (LexicalClass::Boolean, self.boolean),
135 (LexicalClass::Text, self.text),
136 ]
137 }
138
139 pub fn record(&mut self, class: LexicalClass) {
141 match class {
142 LexicalClass::Numeric => self.numeric += 1,
143 LexicalClass::Date => self.date += 1,
144 LexicalClass::Boolean => self.boolean += 1,
145 LexicalClass::Text => self.text += 1,
146 }
147 }
148
149 pub fn classified_count(&self) -> usize {
151 self.numeric + self.date + self.boolean + self.text
152 }
153
154 pub fn dominant(&self) -> Option<(LexicalClass, usize)> {
161 self.counts()
162 .into_iter()
163 .filter(|(_, count)| *count > 0)
164 .fold(None, |best, candidate| match best {
165 Some((_, best_count)) if best_count >= candidate.1 => best,
166 _ => Some(candidate),
167 })
168 }
169
170 pub fn dominant_share(&self) -> Option<f64> {
173 let (_, count) = self.dominant()?;
174 Some(count as f64 / self.classified_count() as f64)
175 }
176
177 pub fn mixture(&self) -> Vec<(LexicalClass, usize, f64)> {
182 let total = self.classified_count();
183 if total == 0 {
184 return Vec::new();
185 }
186 let mut present: Vec<(LexicalClass, usize)> = self
187 .counts()
188 .into_iter()
189 .filter(|(_, count)| *count > 0)
190 .collect();
191 present.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
193 present
194 .into_iter()
195 .map(|(class, count)| (class, count, count as f64 / total as f64))
196 .collect()
197 }
198}
199
200#[derive(
202 Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
203)]
204#[serde(rename_all = "snake_case")]
205pub enum PatternCategory {
206 Contact,
208 Identifier,
210 Network,
212 Geographic,
214 Financial,
216 FilePath,
218 Other,
220}
221
222impl std::fmt::Display for PatternCategory {
223 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224 match self {
225 Self::Contact => write!(f, "contact"),
226 Self::Identifier => write!(f, "identifier"),
227 Self::Network => write!(f, "network"),
228 Self::Geographic => write!(f, "geographic"),
229 Self::Financial => write!(f, "financial"),
230 Self::FilePath => write!(f, "file_path"),
231 Self::Other => write!(f, "other"),
232 }
233 }
234}
235
236#[cfg(test)]
237mod type_homogeneity_tests {
238 use super::*;
239
240 fn homogeneity(numeric: usize, date: usize, boolean: usize, text: usize) -> TypeHomogeneity {
241 TypeHomogeneity {
242 numeric,
243 date,
244 boolean,
245 text,
246 }
247 }
248
249 #[test]
250 fn recording_values_counts_them_by_class() {
251 let mut counts = TypeHomogeneity::default();
252 counts.record(LexicalClass::Numeric);
253 counts.record(LexicalClass::Text);
254 counts.record(LexicalClass::Numeric);
255
256 assert_eq!(counts, homogeneity(2, 0, 0, 1));
257 assert_eq!(counts.classified_count(), 3);
258 assert_eq!(counts.dominant(), Some((LexicalClass::Numeric, 2)));
259 }
260
261 #[test]
262 fn nothing_classified_has_no_dominant_class_and_no_share() {
263 let empty = TypeHomogeneity::default();
267
268 assert_eq!(empty.classified_count(), 0);
269 assert_eq!(empty.dominant(), None);
270 assert_eq!(empty.dominant_share(), None);
271 assert!(empty.mixture().is_empty());
272 }
273
274 #[test]
275 fn a_tie_is_held_by_the_earliest_declared_class() {
276 let tied = homogeneity(50, 50, 0, 0);
280
281 assert_eq!(tied.dominant(), Some((LexicalClass::Numeric, 50)));
282 assert_eq!(tied.dominant_share(), Some(0.5));
283 assert_eq!(
284 tied.mixture(),
285 vec![
286 (LexicalClass::Numeric, 50, 0.5),
287 (LexicalClass::Date, 50, 0.5)
288 ]
289 );
290 }
291
292 #[test]
293 fn mixture_reports_every_present_class_largest_first() {
294 let mixed = homogeneity(60, 10, 0, 30);
295
296 assert_eq!(mixed.dominant_share(), Some(0.6));
297 assert_eq!(
298 mixed.mixture(),
299 vec![
300 (LexicalClass::Numeric, 60, 0.6),
301 (LexicalClass::Text, 30, 0.3),
302 (LexicalClass::Date, 10, 0.1),
303 ],
304 "an absent class must not appear with a 0% share"
305 );
306 }
307
308 #[test]
309 fn counts_survive_a_json_round_trip() {
310 let counts = homogeneity(600, 0, 0, 400);
311 let json = serde_json::to_string(&counts).expect("counts should serialize");
312
313 assert_eq!(json, r#"{"numeric":600,"date":0,"boolean":0,"text":400}"#);
314 assert_eq!(
315 serde_json::from_str::<TypeHomogeneity>(&json).expect("counts should deserialize"),
316 counts
317 );
318 }
319
320 #[test]
321 fn lexical_classes_serialize_as_the_names_reports_use() {
322 for (class, name) in [
323 (LexicalClass::Numeric, "numeric"),
324 (LexicalClass::Date, "date"),
325 (LexicalClass::Boolean, "boolean"),
326 (LexicalClass::Text, "text"),
327 ] {
328 assert_eq!(class.to_string(), name);
329 assert_eq!(
330 serde_json::to_string(&class).expect("class should serialize"),
331 format!("\"{name}\"")
332 );
333 }
334 }
335}
336
337#[cfg(test)]
338mod data_type_name_tests {
339 use super::*;
340
341 #[test]
344 fn every_data_type_has_its_lowercase_name() {
345 for (data_type, name) in [
346 (DataType::String, "string"),
347 (DataType::Identifier, "identifier"),
348 (DataType::Integer, "integer"),
349 (DataType::Float, "float"),
350 (DataType::Date, "date"),
351 (DataType::Boolean, "boolean"),
352 (DataType::Nested, "nested"),
353 ] {
354 assert_eq!(data_type.name(), name);
355 assert_eq!(DataType::from_name(data_type.name()), Some(data_type));
356 }
357 }
358
359 #[test]
360 fn an_unknown_name_has_no_data_type() {
361 assert_eq!(DataType::from_name("decimal"), None);
362 assert_eq!(DataType::from_name("Integer"), None);
363 }
364}