table_to_csv/
date_filter.rs

1use anyhow::{Context, Result};
2use chrono::{NaiveDate, NaiveDateTime, DateTime, FixedOffset};
3
4use crate::types::DateFilter;
5
6/// Parse date filter arguments from command line
7pub fn parse_date_filter(args: &[String]) -> Result<Option<DateFilter>> {
8    // Look for --date-filter flag
9    if let Some(pos) = args.iter().position(|arg| arg == "--date-filter") {
10        // Need at least 2 more arguments: column_name, start_date
11        // end_date is optional and defaults to today
12        if args.len() < pos + 3 {
13            anyhow::bail!(
14                "Error: --date-filter requires at least 2 arguments: <column_name> <start_date> [end_date]\n\
15                Example: --date-filter createdAt 2024-01-01\n\
16                Example: --date-filter createdAt 2024-01-01 2024-12-31"
17            );
18        }
19        
20        let column_name = args[pos + 1].clone();
21        let start_date_str = &args[pos + 2];
22        
23        // Parse start date
24        let start_date = NaiveDate::parse_from_str(start_date_str, "%Y-%m-%d")
25            .context(format!("Invalid start date '{}'. Use format: YYYY-MM-DD", start_date_str))?;
26        
27        // Parse end date or use today's date
28        let end_date = if args.len() > pos + 3 {
29            let end_date_str = &args[pos + 3];
30            NaiveDate::parse_from_str(end_date_str, "%Y-%m-%d")
31                .context(format!("Invalid end date '{}'. Use format: YYYY-MM-DD", end_date_str))?
32        } else {
33            // Default to today's date
34            chrono::Local::now().date_naive()
35        };
36        
37        // Validate date range
38        if start_date > end_date {
39            anyhow::bail!("Error: Start date must be before or equal to end date");
40        }
41        
42        Ok(Some(DateFilter {
43            column_name,
44            start_date,
45            end_date,
46        }))
47    } else {
48        Ok(None)
49    }
50}
51
52/// Apply date filter to rows
53pub fn apply_date_filter(
54    headers: &[String],
55    rows: &[Vec<String>],
56    filter: &DateFilter,
57) -> Result<Vec<Vec<String>>> {
58    // Find the column index for the date column
59    let column_index = headers.iter().position(|h| h == &filter.column_name)
60        .ok_or_else(|| anyhow::anyhow!("Column '{}' not found in table headers", filter.column_name))?;
61    
62    // Filter rows based on date range
63    let filtered: Vec<Vec<String>> = rows.iter()
64        .filter(|row| {
65            if column_index >= row.len() {
66                return false;
67            }
68            
69            let date_value = &row[column_index];
70            
71            // Try to parse the date value
72            match parse_date_value(date_value) {
73                Some(date) => {
74                    date >= filter.start_date && date <= filter.end_date
75                }
76                None => {
77                    eprintln!("Warning: Could not parse date value '{}', excluding row", date_value);
78                    false
79                }
80            }
81        })
82        .cloned()
83        .collect();
84    
85    Ok(filtered)
86}
87
88/// Parse a date value from various formats
89fn parse_date_value(value: &str) -> Option<NaiveDate> {
90    // Try to parse ISO 8601 with timezone first (most common in databases)
91    if let Ok(datetime) = DateTime::<FixedOffset>::parse_from_rfc3339(value) {
92        return Some(datetime.date_naive());
93    }
94    
95    // Try various date formats without timezone
96    let formats = vec![
97        "%Y-%m-%d",           // 2024-01-15
98        "%Y-%m-%d %H:%M:%S",  // 2024-01-15 14:30:00
99        "%Y-%m-%dT%H:%M:%S",  // 2024-01-15T14:30:00 (ISO 8601)
100        "%Y-%m-%d %H:%M:%S%.f", // 2024-01-15 14:30:00.123
101        "%Y-%m-%dT%H:%M:%S%.f", // 2024-01-15T14:30:00.123
102        "%m/%d/%Y",           // 01/15/2024
103        "%d/%m/%Y",           // 15/01/2024
104    ];
105    
106    // Try to parse as NaiveDateTime
107    for format in &formats[..5] {
108        if let Ok(datetime) = NaiveDateTime::parse_from_str(value, format) {
109            return Some(datetime.date());
110        }
111    }
112    
113    // Try to parse as just a date (no time component)
114    for format in &formats {
115        if let Ok(date) = NaiveDate::parse_from_str(value, format) {
116            return Some(date);
117        }
118    }
119    
120    // Try to parse timestamps (milliseconds since epoch)
121    if let Ok(timestamp) = value.parse::<i64>() {
122        // Try both seconds and milliseconds
123        if let Some(datetime) = chrono::DateTime::from_timestamp(timestamp / 1000, 0) {
124            return Some(datetime.date_naive());
125        }
126        if let Some(datetime) = chrono::DateTime::from_timestamp(timestamp, 0) {
127            return Some(datetime.date_naive());
128        }
129    }
130    
131    None
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_parse_date_value() {
140        // ISO 8601 date
141        assert!(parse_date_value("2024-01-15").is_some());
142        
143        // ISO 8601 datetime
144        assert!(parse_date_value("2024-01-15T14:30:00").is_some());
145        
146        // US format
147        assert!(parse_date_value("01/15/2024").is_some());
148        
149        // Invalid date
150        assert!(parse_date_value("not-a-date").is_none());
151    }
152}
153