1use regex::Regex;
7use std::sync::LazyLock;
8
9static DATE_PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
12 vec![
13 Regex::new(r"^(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$").unwrap(),
15 Regex::new(r"^(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])\s+\d{2}:\d{2}:\d{2}$").unwrap(),
17 Regex::new(r"^(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])\s+\d{2}:\d{2}:\d{2}\.\d{1,3}$").unwrap(),
19 Regex::new(r"^(0[1-9]|1[0-2])/(0[1-9]|[12]\d|3[01])/(19|20)\d{2}$").unwrap(),
21 Regex::new(r"^(0[1-9]|[12]\d|3[01])/(0[1-9]|1[0-2])/(19|20)\d{2}$").unwrap(),
23 Regex::new(r"^(0[1-9]|[12]\d|3[01])/(0[1-9]|1[0-2])/(19|20)\d{2}\s+\d{2}:\d{2}:\d{2}$").unwrap(),
25 Regex::new(r"^(0[1-9]|[12]\d|3[01])/(0[1-9]|1[0-2])/(19|20)\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{1,3}$").unwrap(),
27 Regex::new(r"^(0[1-9]|[12]\d|3[01])-(0[1-9]|1[0-2])-(19|20)\d{2}$").unwrap(),
29 Regex::new(r"^(19|20)\d{2}/(0[1-9]|1[0-2])/(0[1-9]|[12]\d|3[01])$").unwrap(),
31 Regex::new(r"^(19|20)\d{2}/(0[1-9]|1[0-2])/(0[1-9]|[12]\d|3[01])\s+\d{2}:\d{2}:\d{2}$").unwrap(),
33 Regex::new(r"^(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T\d{2}:\d{2}:\d{2}")
35 .unwrap(),
36 Regex::new(
38 r"^(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$",
39 )
40 .unwrap(),
41 ]
42});
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum InferredType {
47 Boolean,
48 Integer,
49 Float,
50 DateTime,
51 String,
52 Null,
53}
54
55pub struct TypeInference;
57
58impl TypeInference {
59 #[must_use]
64 pub fn infer_from_string(value: &str) -> InferredType {
65 if value.is_empty() {
67 return InferredType::Null;
68 }
69
70 let value = value.trim();
75
76 if value.eq_ignore_ascii_case("true") || value.eq_ignore_ascii_case("false") {
78 return InferredType::Boolean;
79 }
80
81 if value.parse::<i64>().is_ok() {
83 return InferredType::Integer;
84 }
85
86 if value.parse::<f64>().is_ok() {
88 return InferredType::Float;
89 }
90
91 if Self::looks_like_datetime(value) {
94 return InferredType::DateTime;
95 }
96
97 InferredType::String
99 }
100
101 pub fn looks_like_datetime(value: &str) -> bool {
106 if value.len() < 8 || value.len() > 35 {
108 return false;
109 }
110
111 DATE_PATTERNS.iter().any(|pattern| pattern.is_match(value))
113 }
114
115 #[must_use]
125 pub fn merge_types(type1: InferredType, type2: InferredType) -> InferredType {
126 use InferredType::{Boolean, DateTime, Float, Integer, Null, String};
127
128 match (type1, type2) {
129 (t1, t2) if t1 == t2 => t1,
131
132 (Null, t) | (t, Null) => t,
134
135 (Integer, Float) | (Float, Integer) => Float,
137
138 (Boolean, _) | (_, Boolean) => String,
140
141 (DateTime, _) | (_, DateTime) => String,
143
144 _ => String,
146 }
147 }
148
149 pub fn infer_from_samples<'a, I>(values: I) -> InferredType
154 where
155 I: Iterator<Item = &'a str>,
156 {
157 let mut result_type = InferredType::Null;
158
159 for value in values {
160 let value_type = Self::infer_from_string(value);
161 result_type = Self::merge_types(result_type, value_type);
162
163 if result_type == InferredType::String {
165 break;
166 }
167 }
168
169 result_type
170 }
171
172 #[must_use]
174 pub fn can_coerce_to(value: &str, target_type: InferredType) -> bool {
175 match target_type {
176 InferredType::Boolean => {
177 value.eq_ignore_ascii_case("true")
178 || value.eq_ignore_ascii_case("false")
179 || value == "0"
180 || value == "1"
181 }
182 InferredType::Integer => value.parse::<i64>().is_ok(),
183 InferredType::Float => value.parse::<f64>().is_ok(),
184 InferredType::DateTime => Self::looks_like_datetime(value),
185 InferredType::String => true, InferredType::Null => value.is_empty(),
187 }
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_basic_type_inference() {
197 assert_eq!(
198 TypeInference::infer_from_string("123"),
199 InferredType::Integer
200 );
201 assert_eq!(
202 TypeInference::infer_from_string("123.45"),
203 InferredType::Float
204 );
205 assert_eq!(
206 TypeInference::infer_from_string("true"),
207 InferredType::Boolean
208 );
209 assert_eq!(
210 TypeInference::infer_from_string("FALSE"),
211 InferredType::Boolean
212 );
213 assert_eq!(
214 TypeInference::infer_from_string("hello"),
215 InferredType::String
216 );
217 assert_eq!(TypeInference::infer_from_string(""), InferredType::Null);
218 }
219
220 #[test]
221 fn test_datetime_detection() {
222 assert_eq!(
224 TypeInference::infer_from_string("2024-01-15"),
225 InferredType::DateTime
226 );
227 assert_eq!(
228 TypeInference::infer_from_string("01/15/2024"),
229 InferredType::DateTime
230 );
231 assert_eq!(
232 TypeInference::infer_from_string("15-01-2024"),
233 InferredType::DateTime
234 );
235 assert_eq!(
236 TypeInference::infer_from_string("2024-01-15T10:30:00"),
237 InferredType::DateTime
238 );
239 assert_eq!(
240 TypeInference::infer_from_string("2024-01-15T10:30:00Z"),
241 InferredType::DateTime
242 );
243 }
244
245 #[test]
246 fn test_id_strings_not_detected_as_datetime() {
247 assert_eq!(
249 TypeInference::infer_from_string("BQ-81198596"),
250 InferredType::String
251 );
252 assert_eq!(
253 TypeInference::infer_from_string("ORDER-2024-001"),
254 InferredType::String
255 );
256 assert_eq!(
257 TypeInference::infer_from_string("ID-123-456"),
258 InferredType::String
259 );
260 assert_eq!(
261 TypeInference::infer_from_string("ABC-DEF-GHI"),
262 InferredType::String
263 );
264 assert_eq!(
265 TypeInference::infer_from_string("2024-ABC-123"),
266 InferredType::String
267 );
268 }
269
270 #[test]
271 fn test_invalid_dates_not_detected() {
272 assert_eq!(
274 TypeInference::infer_from_string("2024-13-01"), InferredType::String
276 );
277 assert_eq!(
278 TypeInference::infer_from_string("2024-00-15"), InferredType::String
280 );
281 assert_eq!(
282 TypeInference::infer_from_string("2024-01-32"), InferredType::String
284 );
285 assert_eq!(
286 TypeInference::infer_from_string("2024-01-00"), InferredType::String
288 );
289 }
290
291 #[test]
292 fn test_type_merging() {
293 use InferredType::*;
294
295 assert_eq!(TypeInference::merge_types(Integer, Integer), Integer);
297 assert_eq!(TypeInference::merge_types(String, String), String);
298
299 assert_eq!(TypeInference::merge_types(Null, Integer), Integer);
301 assert_eq!(TypeInference::merge_types(Float, Null), Float);
302
303 assert_eq!(TypeInference::merge_types(Integer, Float), Float);
305 assert_eq!(TypeInference::merge_types(Float, Integer), Float);
306
307 assert_eq!(TypeInference::merge_types(Integer, String), String);
309 assert_eq!(TypeInference::merge_types(DateTime, Integer), String);
310 assert_eq!(TypeInference::merge_types(Boolean, Float), String);
311 }
312
313 #[test]
314 fn test_infer_from_samples() {
315 let samples = vec!["1", "2", "3", "4", "5"];
317 assert_eq!(
318 TypeInference::infer_from_samples(samples.into_iter()),
319 InferredType::Integer
320 );
321
322 let samples = vec!["1", "2.5", "3", "4.0"];
324 assert_eq!(
325 TypeInference::infer_from_samples(samples.into_iter()),
326 InferredType::Float
327 );
328
329 let samples = vec!["1", "hello", "3"];
331 assert_eq!(
332 TypeInference::infer_from_samples(samples.into_iter()),
333 InferredType::String
334 );
335
336 let samples = vec!["", "1", "", "2", "3"];
338 assert_eq!(
339 TypeInference::infer_from_samples(samples.into_iter()),
340 InferredType::Integer
341 );
342 }
343
344 #[test]
345 fn test_can_coerce() {
346 assert!(TypeInference::can_coerce_to("true", InferredType::Boolean));
348 assert!(TypeInference::can_coerce_to("1", InferredType::Boolean));
349 assert!(TypeInference::can_coerce_to("0", InferredType::Boolean));
350 assert!(!TypeInference::can_coerce_to(
351 "hello",
352 InferredType::Boolean
353 ));
354
355 assert!(TypeInference::can_coerce_to("123", InferredType::Integer));
357 assert!(!TypeInference::can_coerce_to(
358 "123.45",
359 InferredType::Integer
360 ));
361 assert!(!TypeInference::can_coerce_to(
362 "hello",
363 InferredType::Integer
364 ));
365
366 assert!(TypeInference::can_coerce_to("123", InferredType::String));
368 assert!(TypeInference::can_coerce_to("hello", InferredType::String));
369 assert!(TypeInference::can_coerce_to("", InferredType::String));
370 }
371}