1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::classification::DataType;
6use crate::pattern::Pattern;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ColumnProfile {
11 pub name: String,
12 pub data_type: DataType,
13 pub null_count: usize,
14 pub total_count: usize,
15 pub unique_count: Option<usize>,
16 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub unique_count_is_approximate: Option<bool>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub invalid_count: Option<usize>,
40 pub stats: ColumnStats,
41 pub patterns: Option<Vec<Pattern>>,
48}
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52pub struct Quartiles {
53 pub q1: f64,
54 pub q2: f64,
55 pub q3: f64,
56 pub iqr: f64,
57}
58
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub struct FrequencyItem {
62 pub value: String,
63 pub count: usize,
64 #[serde(serialize_with = "crate::serde_helpers::round_2")]
65 pub percentage: f64,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct NumericStats {
71 #[serde(serialize_with = "crate::serde_helpers::round_2")]
72 pub min: f64,
73 #[serde(serialize_with = "crate::serde_helpers::round_2")]
74 pub max: f64,
75 #[serde(serialize_with = "crate::serde_helpers::round_4")]
76 pub mean: f64,
77 #[serde(serialize_with = "crate::serde_helpers::round_4")]
78 pub std_dev: f64,
79 #[serde(serialize_with = "crate::serde_helpers::round_4")]
80 pub variance: f64,
81 #[serde(
82 skip_serializing_if = "Option::is_none",
83 serialize_with = "crate::serde_helpers::round_2_opt"
84 )]
85 pub median: Option<f64>,
86 #[serde(
87 skip_serializing_if = "Option::is_none",
88 serialize_with = "crate::serde_helpers::quartiles::serialize"
89 )]
90 pub quartiles: Option<Quartiles>,
91 #[serde(
92 skip_serializing_if = "Option::is_none",
93 serialize_with = "crate::serde_helpers::round_2_opt"
94 )]
95 pub mode: Option<f64>,
96 #[serde(
97 skip_serializing_if = "Option::is_none",
98 serialize_with = "crate::serde_helpers::round_2_opt"
99 )]
100 pub coefficient_of_variation: Option<f64>,
101 #[serde(
102 skip_serializing_if = "Option::is_none",
103 serialize_with = "crate::serde_helpers::round_4_opt"
104 )]
105 pub skewness: Option<f64>,
106 #[serde(
107 skip_serializing_if = "Option::is_none",
108 serialize_with = "crate::serde_helpers::round_4_opt"
109 )]
110 pub kurtosis: Option<f64>,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub is_approximate: Option<bool>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub outlier_count: Option<usize>,
121}
122
123impl NumericStats {
124 pub fn empty() -> Self {
125 Self {
126 min: 0.0,
127 max: 0.0,
128 mean: 0.0,
129 std_dev: 0.0,
130 variance: 0.0,
131 median: None,
132 quartiles: None,
133 mode: None,
134 coefficient_of_variation: None,
135 skewness: None,
136 kurtosis: None,
137 is_approximate: None,
138 outlier_count: None,
139 }
140 }
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct TextStats {
146 pub min_length: usize,
147 pub max_length: usize,
148 #[serde(serialize_with = "crate::serde_helpers::round_2")]
149 pub avg_length: f64,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub most_frequent: Option<Vec<FrequencyItem>>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub least_frequent: Option<Vec<FrequencyItem>>,
154}
155
156impl TextStats {
157 pub fn empty() -> Self {
158 Self {
159 min_length: 0,
160 max_length: 0,
161 avg_length: 0.0,
162 most_frequent: None,
163 least_frequent: None,
164 }
165 }
166
167 pub fn from_lengths(min_length: usize, max_length: usize, avg_length: f64) -> Self {
168 Self {
169 min_length: if min_length == usize::MAX {
170 0
171 } else {
172 min_length
173 },
174 max_length,
175 avg_length,
176 most_frequent: None,
177 least_frequent: None,
178 }
179 }
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct DateTimeStats {
185 pub min_datetime: String,
186 pub max_datetime: String,
187 #[serde(serialize_with = "crate::serde_helpers::round_2")]
188 pub duration_days: f64,
189 pub year_distribution: HashMap<i32, usize>,
190 pub month_distribution: HashMap<u32, usize>,
191 pub day_of_week_distribution: HashMap<String, usize>,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub hour_distribution: Option<HashMap<u32, usize>>,
194}
195
196impl DateTimeStats {
197 pub fn empty() -> Self {
198 Self {
199 min_datetime: String::new(),
200 max_datetime: String::new(),
201 duration_days: 0.0,
202 year_distribution: HashMap::new(),
203 month_distribution: HashMap::new(),
204 day_of_week_distribution: HashMap::new(),
205 hour_distribution: None,
206 }
207 }
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct BooleanStats {
213 pub true_count: usize,
214 pub false_count: usize,
215 #[serde(serialize_with = "crate::serde_helpers::round_4")]
216 pub true_ratio: f64,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub enum ColumnStats {
222 Numeric(NumericStats),
223 Text(TextStats),
224 DateTime(DateTimeStats),
225 Boolean(BooleanStats),
226 None,
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_column_profile_json_roundtrip() {
235 let profile = ColumnProfile {
236 name: "test_col".to_string(),
237 data_type: DataType::Integer,
238 null_count: 2,
239 total_count: 10,
240 unique_count: Some(8),
241 unique_count_is_approximate: Some(false),
242 invalid_count: Some(0),
243 stats: ColumnStats::Numeric(NumericStats {
244 min: 1.0,
245 max: 100.0,
246 mean: 50.5,
247 std_dev: 28.87,
248 variance: 833.25,
249 median: Some(50.0),
250 quartiles: Some(Quartiles {
251 q1: 25.0,
252 q2: 50.0,
253 q3: 75.0,
254 iqr: 50.0,
255 }),
256 mode: Some(42.0),
257 coefficient_of_variation: Some(57.17),
258 skewness: Some(0.0),
259 kurtosis: Some(-1.2),
260 is_approximate: Some(false),
261 outlier_count: Some(0),
262 }),
263 patterns: Some(vec![]),
264 };
265
266 let json = serde_json::to_string(&profile).unwrap();
267 let deserialized: ColumnProfile = serde_json::from_str(&json).unwrap();
268
269 assert_eq!(deserialized.name, "test_col");
270 assert_eq!(deserialized.data_type, DataType::Integer);
271 assert_eq!(deserialized.total_count, 10);
272 assert_eq!(deserialized.null_count, 2);
273 assert_eq!(deserialized.unique_count_is_approximate, Some(false));
274
275 if let ColumnStats::Numeric(n) = &deserialized.stats {
276 assert!((n.min - 1.0).abs() < 0.01);
277 assert!((n.max - 100.0).abs() < 0.01);
278 assert!((n.mean - 50.5).abs() < 0.01);
279 assert!(n.median.is_some());
280 assert!(n.quartiles.is_some());
281 } else {
282 panic!("Expected Numeric stats after roundtrip");
283 }
284 }
285
286 #[test]
287 fn test_text_stats_json_roundtrip() {
288 let profile = ColumnProfile {
289 name: "name".to_string(),
290 data_type: DataType::String,
291 null_count: 0,
292 total_count: 3,
293 unique_count: Some(3),
294 unique_count_is_approximate: Some(false),
295 invalid_count: None,
296 stats: ColumnStats::Text(TextStats {
297 min_length: 3,
298 max_length: 7,
299 avg_length: 5.0,
300 most_frequent: None,
301 least_frequent: None,
302 }),
303 patterns: Some(vec![]),
304 };
305
306 let json = serde_json::to_string(&profile).unwrap();
307 let deserialized: ColumnProfile = serde_json::from_str(&json).unwrap();
308
309 assert_eq!(deserialized.data_type, DataType::String);
310 if let ColumnStats::Text(t) = &deserialized.stats {
311 assert_eq!(t.min_length, 3);
312 assert_eq!(t.max_length, 7);
313 } else {
314 panic!("Expected Text stats after roundtrip");
315 }
316 }
317}