sql_cli/data/
value_parsing.rs

1/// Centralized value parsing utilities for consistent type detection and conversion
2use crate::data::datatable::DataValue;
3use crate::sql::functions::date_time::parse_datetime;
4
5/// Parse a string into a boolean value using consistent rules across the codebase
6/// Accepts: true/false, t/f, yes/no, y/n, 1/0 (case-insensitive except for numbers)
7pub fn parse_bool(s: &str) -> Option<bool> {
8    match s.to_lowercase().as_str() {
9        "true" | "t" | "yes" | "y" => Some(true),
10        "false" | "f" | "no" | "n" => Some(false),
11        "1" => Some(true),
12        "0" => Some(false),
13        _ => None,
14    }
15}
16
17/// Parse a string into a boolean for CSV loading (excludes numeric forms)
18/// Only accepts: true/false, t/f, yes/no, y/n (case-insensitive)
19pub fn parse_bool_strict(s: &str) -> Option<bool> {
20    match s.to_lowercase().as_str() {
21        "true" | "t" | "yes" | "y" => Some(true),
22        "false" | "f" | "no" | "n" => Some(false),
23        _ => None,
24    }
25}
26
27/// Check if a string can be parsed as a boolean
28pub fn is_bool_str(s: &str) -> bool {
29    parse_bool(s).is_some()
30}
31
32/// Check if a DataValue can be considered a boolean
33pub fn is_bool_value(value: &DataValue) -> bool {
34    match value {
35        DataValue::Boolean(_) => true,
36        DataValue::String(s) => is_bool_str(s),
37        DataValue::InternedString(s) => is_bool_str(s.as_str()),
38        DataValue::Integer(i) => *i == 0 || *i == 1,
39        _ => false,
40    }
41}
42
43/// Parse a string value into the appropriate DataValue type
44/// Order of precedence: Null -> Boolean (text only) -> Integer -> Float -> DateTime -> String
45/// Note: "1" and "0" are parsed as integers, not booleans, for data consistency
46pub fn parse_value(field: &str, is_null: bool) -> DataValue {
47    // Handle null/empty cases
48    if is_null {
49        return DataValue::Null;
50    }
51
52    if field.is_empty() {
53        return DataValue::String(String::new());
54    }
55
56    // Try boolean first, but only text forms (not numeric "1"/"0")
57    if let Some(b) = parse_bool_strict(field) {
58        return DataValue::Boolean(b);
59    }
60
61    // Try integer
62    if let Ok(i) = field.parse::<i64>() {
63        return DataValue::Integer(i);
64    }
65
66    // Try float
67    if let Ok(f) = field.parse::<f64>() {
68        return DataValue::Float(f);
69    }
70
71    // Try date/time if it looks like one
72    if looks_like_datetime(field) {
73        if let Ok(dt) = parse_datetime(field) {
74            // Store as ISO 8601 string for consistent comparisons
75            return DataValue::DateTime(dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string());
76        }
77    }
78
79    // Default to string
80    DataValue::String(field.to_string())
81}
82
83/// Check if a string looks like it might be a date/time
84fn looks_like_datetime(field: &str) -> bool {
85    // Must contain date separators (-, /, or T) and be reasonable length
86    (field.contains('-') || field.contains('/') || field.contains('T'))
87        && field.len() >= 8  // Minimum for a date like "1/1/2024"
88        && field.len() <= 30 // Maximum reasonable date length
89        && !field.starts_with("--") // Avoid things like "--option"
90        && field.chars().filter(|c| c.is_ascii_digit()).count() >= 4 // At least 4 digits
91}