table_to_csv/
date_filter.rs1use anyhow::{Context, Result};
2use chrono::{NaiveDate, NaiveDateTime, DateTime, FixedOffset};
3
4use crate::types::DateFilter;
5
6pub fn parse_date_filter(args: &[String]) -> Result<Option<DateFilter>> {
8 if let Some(pos) = args.iter().position(|arg| arg == "--date-filter") {
10 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 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 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 chrono::Local::now().date_naive()
35 };
36
37 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
52pub fn apply_date_filter(
54 headers: &[String],
55 rows: &[Vec<String>],
56 filter: &DateFilter,
57) -> Result<Vec<Vec<String>>> {
58 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 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 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
88fn parse_date_value(value: &str) -> Option<NaiveDate> {
90 if let Ok(datetime) = DateTime::<FixedOffset>::parse_from_rfc3339(value) {
92 return Some(datetime.date_naive());
93 }
94
95 let formats = vec![
97 "%Y-%m-%d", "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S%.f", "%Y-%m-%dT%H:%M:%S%.f", "%m/%d/%Y", "%d/%m/%Y", ];
105
106 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 for format in &formats {
115 if let Ok(date) = NaiveDate::parse_from_str(value, format) {
116 return Some(date);
117 }
118 }
119
120 if let Ok(timestamp) = value.parse::<i64>() {
122 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 assert!(parse_date_value("2024-01-15").is_some());
142
143 assert!(parse_date_value("2024-01-15T14:30:00").is_some());
145
146 assert!(parse_date_value("01/15/2024").is_some());
148
149 assert!(parse_date_value("not-a-date").is_none());
151 }
152}
153