1use std::collections::HashMap;
40use std::fmt;
41
42use rich::{Console, ConsoleOptions, Justify, Renderable, Segment, Table};
43
44use crate::{DataType, Field, Rows, Schema, Value};
45
46#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
48pub enum InferredType {
49 Null,
51 Boolean,
53 Integer,
55 Float,
57 Date,
59 Timestamp,
61 Text,
63}
64
65impl InferredType {
66 pub fn name(self) -> &'static str {
68 match self {
69 InferredType::Null => "null",
70 InferredType::Boolean => "boolean",
71 InferredType::Integer => "integer",
72 InferredType::Float => "float",
73 InferredType::Date => "date",
74 InferredType::Timestamp => "timestamp",
75 InferredType::Text => "text",
76 }
77 }
78
79 pub fn data_type(self) -> DataType {
82 match self {
83 InferredType::Null => DataType::Unknown,
84 InferredType::Boolean => DataType::Boolean,
85 InferredType::Integer => DataType::Integer,
86 InferredType::Float => DataType::Float,
87 InferredType::Date => DataType::Date,
88 InferredType::Timestamp => DataType::timestamp(),
89 InferredType::Text => DataType::String,
90 }
91 }
92}
93
94impl fmt::Display for InferredType {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str(self.name())
97 }
98}
99
100#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
104pub struct Evidence {
105 pub cells: usize,
107 pub nulls: usize,
109 pub booleans: usize,
111 pub integers: usize,
113 pub floats: usize,
115 pub dates: usize,
117 pub timestamps: usize,
119}
120
121impl Evidence {
122 pub fn values(&self) -> usize {
124 self.cells - self.nulls
125 }
126
127 pub fn parsed(&self, data_type: InferredType) -> usize {
129 match data_type {
130 InferredType::Null => self.nulls,
131 InferredType::Boolean => self.booleans,
132 InferredType::Integer => self.integers,
133 InferredType::Float => self.floats,
134 InferredType::Date => self.dates,
135 InferredType::Timestamp => self.timestamps,
136 InferredType::Text => self.values(),
137 }
138 }
139
140 pub fn best(&self) -> InferredType {
142 let values = self.values();
143 if values == 0 {
144 InferredType::Null
145 } else if self.booleans == values {
146 InferredType::Boolean
147 } else if self.integers == values {
148 InferredType::Integer
149 } else if self.floats == values {
150 InferredType::Float
151 } else if self.dates == values {
152 InferredType::Date
153 } else if self.timestamps == values {
154 InferredType::Timestamp
155 } else {
156 InferredType::Text
157 }
158 }
159
160 pub fn closest(&self) -> Option<(InferredType, usize)> {
163 [
164 (InferredType::Integer, self.integers),
165 (InferredType::Float, self.floats),
166 (InferredType::Boolean, self.booleans),
167 (InferredType::Date, self.dates),
168 (InferredType::Timestamp, self.timestamps),
169 ]
170 .into_iter()
171 .filter(|(_, n)| *n > 0)
172 .fold(
175 None,
176 |best: Option<(InferredType, usize)>, (t, n)| match best {
177 Some((_, m)) if m >= n => best,
178 _ => Some((t, n)),
179 },
180 )
181 }
182}
183
184#[derive(Clone, Debug, PartialEq, Eq)]
186pub struct ColumnInference {
187 name: String,
188 data_type: InferredType,
189 evidence: Evidence,
190 overridden: bool,
191}
192
193impl ColumnInference {
194 pub fn name(&self) -> &str {
196 &self.name
197 }
198
199 pub fn data_type(&self) -> InferredType {
201 self.data_type
202 }
203
204 pub fn evidence(&self) -> &Evidence {
206 &self.evidence
207 }
208
209 pub fn is_overridden(&self) -> bool {
211 self.overridden
212 }
213
214 pub fn summary(&self) -> String {
218 let e = &self.evidence;
219 let mut out = self.data_type.name().to_string();
220 if self.overridden {
221 out.push_str(" (override)");
222 }
223 let values = e.values();
224 let plural = |n: usize| if n == 1 { "value" } else { "values" };
225 match self.data_type {
226 InferredType::Null => out.push_str(&format!(": {} null", e.nulls)),
227 InferredType::Text if !self.overridden => match e.closest() {
228 Some((t, n)) => out.push_str(&format!(
229 ": {n} of {values} {} parse as {t}",
230 plural(values)
231 )),
232 None => out.push_str(&format!(": {values} {}", plural(values))),
233 },
234 t => out.push_str(&format!(": {} of {values} {}", e.parsed(t), plural(values))),
235 }
236 if e.nulls > 0 && self.data_type != InferredType::Null {
237 out.push_str(&format!(", {} null", e.nulls));
238 }
239 out
240 }
241}
242
243pub const DEFAULT_NULL_TOKENS: [&str; 5] = ["", "null", "NULL", "NA", "N/A"];
246
247#[derive(Clone, Debug)]
249pub struct Inferrer {
250 nulls: Vec<String>,
251 overrides: HashMap<String, InferredType>,
252}
253
254impl Default for Inferrer {
255 fn default() -> Self {
256 Inferrer {
257 nulls: DEFAULT_NULL_TOKENS.map(String::from).to_vec(),
258 overrides: HashMap::new(),
259 }
260 }
261}
262
263impl Inferrer {
264 pub fn new() -> Self {
266 Inferrer::default()
267 }
268
269 pub fn null_tokens(mut self, tokens: impl IntoIterator<Item = impl Into<String>>) -> Self {
271 self.nulls = tokens.into_iter().map(Into::into).collect();
272 self
273 }
274
275 pub fn override_type(mut self, column: impl Into<String>, data_type: InferredType) -> Self {
277 self.overrides.insert(column.into(), data_type);
278 self
279 }
280
281 pub fn infer(&self, rows: &Rows) -> Inference {
283 let columns = rows
284 .columns()
285 .iter()
286 .enumerate()
287 .map(|(index, name)| {
288 let mut evidence = Evidence::default();
289 for value in rows.column(index) {
290 self.count(value, &mut evidence);
291 }
292 let (data_type, overridden) = match self.overrides.get(name) {
293 Some(&t) => (t, true),
294 None => (evidence.best(), false),
295 };
296 ColumnInference {
297 name: name.clone(),
298 data_type,
299 evidence,
300 overridden,
301 }
302 })
303 .collect();
304 Inference {
305 columns,
306 nulls: self.nulls.clone(),
307 }
308 }
309
310 fn count(&self, value: &Value, e: &mut Evidence) {
311 e.cells += 1;
312 match value {
313 Value::Null => e.nulls += 1,
314 Value::Int(_) => {
315 e.integers += 1;
316 e.floats += 1;
317 }
318 Value::Float(_) => e.floats += 1,
319 Value::Str(_) | Value::Text(_) => {
320 let text = value.plain();
321 let text = text.trim();
322 if is_null(&self.nulls, text) {
323 e.nulls += 1;
324 return;
325 }
326 if parse_bool(text).is_some() {
327 e.booleans += 1;
328 }
329 if parse_int(text).is_some() {
330 e.integers += 1;
331 }
332 if parse_float(text).is_some() {
333 e.floats += 1;
334 }
335 if is_date(text) {
336 e.dates += 1;
337 e.timestamps += 1;
338 } else if is_timestamp(text) {
339 e.timestamps += 1;
340 }
341 }
342 }
343 }
344}
345
346fn is_null(tokens: &[String], text: &str) -> bool {
347 tokens.iter().any(|t| t == text)
348}
349
350#[derive(Clone, Debug, PartialEq, Eq)]
352pub struct Inference {
353 columns: Vec<ColumnInference>,
354 nulls: Vec<String>,
355}
356
357impl Inference {
358 pub fn columns(&self) -> &[ColumnInference] {
360 &self.columns
361 }
362
363 pub fn schema(&self) -> Schema {
366 Schema::new(self.columns.iter().map(|c| {
367 Field::new(c.name.clone(), c.data_type.data_type()).nullable(c.evidence.nulls > 0)
368 }))
369 }
370
371 pub fn apply(&self, rows: &mut Rows) {
378 let types: Vec<InferredType> = self.columns.iter().map(|c| c.data_type).collect();
379 for row in rows.rows_mut() {
380 for (cell, &data_type) in row.iter_mut().zip(&types) {
381 if data_type == InferredType::Text {
382 continue;
383 }
384 let converted = match &*cell {
385 Value::Str(_) | Value::Text(_) => {
386 let plain = cell.plain();
387 let text = plain.trim();
388 if is_null(&self.nulls, text) {
389 Some(Value::Null)
390 } else {
391 match data_type {
392 InferredType::Integer => parse_int(text).map(Value::Int),
393 InferredType::Float => parse_float(text).map(Value::Float),
394 _ => None,
395 }
396 }
397 }
398 Value::Int(n) if data_type == InferredType::Float => {
399 Some(Value::Float(*n as f64))
400 }
401 _ => None,
402 };
403 if let Some(converted) = converted {
404 *cell = converted;
405 }
406 }
407 }
408 rows.set_schema(Some(self.schema()));
409 }
410}
411
412impl Renderable for Inference {
414 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
415 self.to_table().rich_render(console, options)
416 }
417
418 fn measure(&self, console: &Console, options: &ConsoleOptions) -> rich::measure::Measurement {
419 self.to_table().measure(console, options)
420 }
421}
422
423impl Inference {
424 pub fn to_table(&self) -> Table {
426 let mut table = Table::new();
427 table.add_column("column");
428 table.add_column("type");
429 table.add_column_justify("parsed", Justify::Right);
430 table.add_column_justify("nulls", Justify::Right);
431 for c in &self.columns {
432 let mut kind = c.data_type.name().to_string();
433 if c.overridden {
434 kind.push_str(" (override)");
435 }
436 let parsed = match c.data_type {
437 InferredType::Null => String::new(),
438 InferredType::Text => match c.evidence.closest() {
439 Some((t, n)) if !c.overridden => format!("{n}/{} {t}", c.evidence.values()),
440 _ => format!("{}/{}", c.evidence.values(), c.evidence.values()),
441 },
442 t => format!("{}/{}", c.evidence.parsed(t), c.evidence.values()),
443 };
444 table.add_row_text(vec![
445 rich::Text::new(c.name.clone()),
446 rich::Text::new(kind),
447 rich::Text::new(parsed),
448 rich::Text::new(c.evidence.nulls.to_string()),
449 ]);
450 }
451 table
452 }
453}
454
455pub fn parse_bool(text: &str) -> Option<bool> {
457 if text.eq_ignore_ascii_case("true") {
458 Some(true)
459 } else if text.eq_ignore_ascii_case("false") {
460 Some(false)
461 } else {
462 None
463 }
464}
465
466pub fn parse_int(text: &str) -> Option<i64> {
468 let digits = text.strip_prefix(['+', '-']).unwrap_or(text);
469 if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
470 return None;
471 }
472 text.parse().ok()
473}
474
475pub fn parse_float(text: &str) -> Option<f64> {
478 let body = text.strip_prefix(['+', '-']).unwrap_or(text);
479 let (mantissa, exponent) = match body.find(['e', 'E']) {
480 Some(at) => (&body[..at], Some(&body[at + 1..])),
481 None => (body, None),
482 };
483 let (whole, fraction) = mantissa.split_once('.').unwrap_or((mantissa, ""));
484 let digits = |s: &str| s.bytes().all(|b| b.is_ascii_digit());
485 if whole.len() + fraction.len() == 0 || !digits(whole) || !digits(fraction) {
486 return None;
487 }
488 if let Some(exponent) = exponent {
489 let exponent = exponent.strip_prefix(['+', '-']).unwrap_or(exponent);
490 if exponent.is_empty() || !digits(exponent) {
491 return None;
492 }
493 }
494 text.parse().ok()
495}
496
497pub fn is_date(text: &str) -> bool {
499 let b = text.as_bytes();
500 if b.len() != 10 || b[4] != b'-' || b[7] != b'-' {
501 return false;
502 }
503 let number = |s: &[u8]| -> Option<u32> {
504 s.iter().try_fold(0u32, |n, &c| {
505 c.is_ascii_digit().then(|| n * 10 + u32::from(c - b'0'))
506 })
507 };
508 let (Some(year), Some(month), Some(day)) = (number(&b[..4]), number(&b[5..7]), number(&b[8..]))
509 else {
510 return false;
511 };
512 let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
513 let days = match month {
514 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
515 4 | 6 | 9 | 11 => 30,
516 2 if leap => 29,
517 2 => 28,
518 _ => return false,
519 };
520 (1..=days).contains(&day)
521}
522
523pub fn is_timestamp(text: &str) -> bool {
526 if text.len() < 16 || !text.is_char_boundary(10) || !is_date(&text[..10]) {
527 return false;
528 }
529 let rest = &text.as_bytes()[10..];
530 if rest[0] != b'T' && rest[0] != b't' && rest[0] != b' ' {
531 return false;
532 }
533 let mut time = &rest[1..];
534 let two = |s: &[u8], max: u32| -> bool {
535 s.len() >= 2
536 && s[0].is_ascii_digit()
537 && s[1].is_ascii_digit()
538 && u32::from(s[0] - b'0') * 10 + u32::from(s[1] - b'0') <= max
539 };
540 if !(two(time, 23) && time.get(2) == Some(&b':') && two(&time[3..], 59)) {
541 return false;
542 }
543 time = &time[5..];
544 if time.first() == Some(&b':') {
545 if !two(&time[1..], 60) {
546 return false;
547 }
548 time = &time[3..];
549 if time.first() == Some(&b'.') {
550 let digits = time[1..].iter().take_while(|c| c.is_ascii_digit()).count();
551 if digits == 0 {
552 return false;
553 }
554 time = &time[1 + digits..];
555 }
556 }
557 match time {
558 [] | [b'Z'] | [b'z'] => true,
559 [b'+' | b'-', h1, h2, b':', m1, m2] | [b'+' | b'-', h1, h2, m1, m2] => {
560 two(&[*h1, *h2], 23) && two(&[*m1, *m2], 59)
561 }
562 _ => false,
563 }
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569
570 #[test]
571 fn numbers_parse_strictly() {
572 assert_eq!(parse_int("-42"), Some(-42));
573 assert_eq!(parse_int("+7"), Some(7));
574 assert_eq!(parse_int("1.0"), None);
575 assert_eq!(parse_int("99999999999999999999"), None);
576 assert_eq!(parse_float("99999999999999999999"), Some(1e20));
577 assert_eq!(parse_float(".5"), Some(0.5));
578 assert_eq!(parse_float("5."), Some(5.0));
579 assert_eq!(parse_float("-1.5e-3"), Some(-0.0015));
580 for text in ["inf", "NaN", ".", "-", "1e", "1_000", "0x10", " 1", "1.2.3"] {
581 assert_eq!(parse_float(text), None, "{text}");
582 }
583 assert_eq!(parse_bool("TRUE"), Some(true));
584 assert_eq!(parse_bool("yes"), None);
585 }
586
587 #[test]
588 fn dates_and_timestamps_are_checked() {
589 assert!(is_date("2024-02-29"));
590 assert!(!is_date("2023-02-29"));
591 assert!(!is_date("2024-13-01"));
592 assert!(!is_date("2024-1-01"));
593 for ok in [
594 "2024-02-29T23:59",
595 "2024-02-29 23:59:60",
596 "2024-02-29T00:00:00.123456Z",
597 "2024-02-29T00:00:00+05:30",
598 "2024-02-29T00:00-0800",
599 ] {
600 assert!(is_timestamp(ok), "{ok}");
601 }
602 for bad in [
603 "2024-02-29",
604 "2024-02-29T24:00",
605 "2024-02-29T10:00:00.",
606 "2024-02-29T10:00+5",
607 "2024-02-29X10:00",
608 ] {
609 assert!(!is_timestamp(bad), "{bad}");
610 }
611 }
612
613 #[test]
614 fn columns_take_the_narrowest_type_and_overrides_win() {
615 let mut rows = Rows::new(["n", "x", "b", "empty", "mixed"]);
616 rows.push([
617 "1".into(),
618 "1".into(),
619 "true".into(),
620 "".into(),
621 Value::Int(3),
622 ]);
623 rows.push([
624 "2".into(),
625 "2.5".into(),
626 "False".into(),
627 "NA".into(),
628 "x".into(),
629 ]);
630 let inference = Inferrer::new()
631 .override_type("n", InferredType::Float)
632 .infer(&rows);
633 let types: Vec<InferredType> = inference.columns().iter().map(|c| c.data_type()).collect();
634 assert_eq!(
635 types,
636 [
637 InferredType::Float,
638 InferredType::Float,
639 InferredType::Boolean,
640 InferredType::Null,
641 InferredType::Text
642 ]
643 );
644 assert!(inference.columns()[0].is_overridden());
645 assert_eq!(
646 inference.columns()[0].summary(),
647 "float (override): 2 of 2 values"
648 );
649 assert_eq!(inference.columns()[3].summary(), "null: 2 null");
650 let schema = inference.schema();
651 assert!(schema.fields()[3].is_nullable());
652 assert!(!schema.fields()[0].is_nullable());
653
654 inference.apply(&mut rows);
655 assert_eq!(rows.rows()[0][0], Value::Float(1.0));
656 assert_eq!(rows.rows()[1][1], Value::Float(2.5));
657 assert_eq!(rows.rows()[1][2], Value::from("False"));
658 assert_eq!(rows.rows()[1][3], Value::Null);
659 assert_eq!(rows.rows()[0][4], Value::Int(3));
660 assert_eq!(
661 rows.schema().unwrap().fields()[2].data_type(),
662 &DataType::Boolean
663 );
664 }
665
666 #[test]
667 fn the_report_is_a_table() {
668 let mut rows = Rows::new(["id"]);
669 rows.push(["1".into()]);
670 rows.push(["x".into()]);
671 let inference = Inferrer::new().infer(&rows);
672 let out = Console::builder()
673 .width(40)
674 .build()
675 .render_export(&inference);
676 assert!(
677 out.contains("│ id │ text │ 1/2 integer │ 0 │"),
678 "{out}"
679 );
680 }
681}