sql_cli/ui/utils/
column_utils.rs

1/// Column utilities extracted from enhanced_tui
2/// Contains column statistics, width calculations, and column data extraction
3use crate::buffer::ColumnStatistics;
4use crate::data::data_provider::DataProvider;
5use crate::data_analyzer::{self, DataAnalyzer};
6use std::collections::HashMap;
7
8/// Calculate statistics for a specific column
9pub fn calculate_column_statistics(
10    provider: &dyn DataProvider,
11    analyzer: &mut DataAnalyzer,
12    column_index: usize,
13) -> Option<ColumnStatistics> {
14    let headers = provider.get_column_names();
15    if headers.is_empty() || column_index >= headers.len() {
16        return None;
17    }
18
19    let column_name = headers[column_index].clone();
20    let row_count = provider.get_row_count();
21
22    // Extract column data
23    let mut column_data = Vec::with_capacity(row_count);
24    for row_idx in 0..row_count {
25        if let Some(row) = provider.get_row(row_idx) {
26            if column_index < row.len() {
27                column_data.push(row[column_index].clone());
28            } else {
29                column_data.push(String::new());
30            }
31        }
32    }
33
34    // Convert to references for the analyzer
35    let data_refs: Vec<&str> = column_data.iter().map(|s| s.as_str()).collect();
36
37    // Calculate statistics
38    let analyzer_stats = analyzer.calculate_column_statistics(&column_name, &data_refs);
39
40    // Convert to buffer's ColumnStatistics format
41    Some(ColumnStatistics {
42        column_name: analyzer_stats.column_name,
43        column_type: match analyzer_stats.data_type {
44            data_analyzer::ColumnType::Integer | data_analyzer::ColumnType::Float => {
45                crate::buffer::ColumnType::Numeric
46            }
47            data_analyzer::ColumnType::String
48            | data_analyzer::ColumnType::Boolean
49            | data_analyzer::ColumnType::Date
50            | data_analyzer::ColumnType::Unknown => crate::buffer::ColumnType::String,
51            data_analyzer::ColumnType::Mixed => crate::buffer::ColumnType::Mixed,
52        },
53        total_count: analyzer_stats.total_values,
54        null_count: analyzer_stats.null_values,
55        unique_count: analyzer_stats.unique_values,
56        frequency_map: analyzer_stats.frequency_map,
57        min: analyzer_stats.min_value.and_then(|s| s.parse::<f64>().ok()),
58        max: analyzer_stats.max_value.and_then(|s| s.parse::<f64>().ok()),
59        sum: analyzer_stats.sum_value,
60        mean: analyzer_stats.avg_value,
61        median: analyzer_stats.median_value,
62    })
63}
64
65/// Calculate optimal column widths based on content
66pub fn calculate_optimal_column_widths(
67    provider: &dyn DataProvider,
68    max_sample_rows: usize,
69) -> Vec<u16> {
70    let column_count = provider.get_column_count();
71    let row_count = provider.get_row_count();
72    let sample_size = row_count.min(max_sample_rows);
73
74    let mut column_widths = vec![0u16; column_count];
75
76    // Start with header widths
77    let headers = provider.get_column_names();
78    for (i, header) in headers.iter().enumerate() {
79        if i < column_widths.len() {
80            column_widths[i] = header.len() as u16;
81        }
82    }
83
84    // Sample rows to find max widths
85    for row_idx in 0..sample_size {
86        if let Some(row) = provider.get_row(row_idx) {
87            for (col_idx, cell) in row.iter().enumerate() {
88                if col_idx < column_widths.len() {
89                    column_widths[col_idx] = column_widths[col_idx].max(cell.len() as u16);
90                }
91            }
92        }
93    }
94
95    // Apply constraints
96    const MIN_WIDTH: u16 = 5;
97    const MAX_WIDTH: u16 = 50;
98
99    for width in &mut column_widths {
100        *width = (*width).clamp(MIN_WIDTH, MAX_WIDTH);
101    }
102
103    column_widths
104}
105
106/// Calculate column widths for a specific viewport range
107pub fn calculate_viewport_column_widths(
108    provider: &dyn DataProvider,
109    viewport_start: usize,
110    viewport_end: usize,
111    max_sample_rows: usize,
112) -> HashMap<usize, u16> {
113    let mut widths = HashMap::new();
114    let row_count = provider.get_row_count();
115    let sample_size = row_count.min(max_sample_rows);
116
117    // Get headers
118    let headers = provider.get_column_names();
119
120    // Initialize with header widths for viewport columns
121    for col_idx in viewport_start..viewport_end.min(headers.len()) {
122        widths.insert(col_idx, headers[col_idx].len() as u16);
123    }
124
125    // Sample rows to find max widths
126    for row_idx in 0..sample_size {
127        if let Some(row) = provider.get_row(row_idx) {
128            for col_idx in viewport_start..viewport_end.min(row.len()) {
129                let current_width = widths.get(&col_idx).copied().unwrap_or(0);
130                let cell_width = row[col_idx].len() as u16;
131                widths.insert(col_idx, current_width.max(cell_width));
132            }
133        }
134    }
135
136    // Apply constraints
137    const MIN_WIDTH: u16 = 5;
138    const MAX_WIDTH: u16 = 50;
139
140    for width in widths.values_mut() {
141        *width = (*width).clamp(MIN_WIDTH, MAX_WIDTH);
142    }
143
144    widths
145}
146
147/// Extract all values from a specific column
148pub fn extract_column_values(provider: &dyn DataProvider, column_index: usize) -> Vec<String> {
149    let row_count = provider.get_row_count();
150    let mut values = Vec::with_capacity(row_count);
151
152    for row_idx in 0..row_count {
153        if let Some(row) = provider.get_row(row_idx) {
154            if column_index < row.len() {
155                values.push(row[column_index].clone());
156            } else {
157                values.push(String::new());
158            }
159        }
160    }
161
162    values
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    // Mock implementation for testing
170    #[derive(Debug)]
171    struct MockDataProvider {
172        headers: Vec<String>,
173        rows: Vec<Vec<String>>,
174    }
175
176    impl DataProvider for MockDataProvider {
177        fn get_column_count(&self) -> usize {
178            self.headers.len()
179        }
180
181        fn get_row_count(&self) -> usize {
182            self.rows.len()
183        }
184
185        fn get_column_names(&self) -> Vec<String> {
186            self.headers.clone()
187        }
188
189        fn get_row(&self, index: usize) -> Option<Vec<String>> {
190            self.rows.get(index).cloned()
191        }
192
193        fn get_cell_value(&self, row: usize, col: usize) -> Option<String> {
194            self.rows.get(row).and_then(|r| r.get(col)).cloned()
195        }
196    }
197
198    #[test]
199    fn test_calculate_optimal_widths() {
200        let provider = MockDataProvider {
201            headers: vec![
202                "ID".to_string(),
203                "Name".to_string(),
204                "Description".to_string(),
205            ],
206            rows: vec![
207                vec!["1".to_string(), "Alice".to_string(), "Short".to_string()],
208                vec![
209                    "2".to_string(),
210                    "Bob".to_string(),
211                    "A very long description that should be clamped".to_string(),
212                ],
213            ],
214        };
215
216        let widths = calculate_optimal_column_widths(&provider, 10);
217        assert_eq!(widths.len(), 3);
218        assert!(widths[0] >= 2); // ID
219        assert!(widths[1] >= 4); // Name
220        assert!(widths[2] <= 50); // Description should be clamped
221    }
222}