boxmux_lib/
table.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// Table data structure for enhanced data visualization
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct TableData {
7    pub headers: Vec<String>,
8    pub rows: Vec<Vec<String>>,
9    pub metadata: HashMap<String, String>,
10}
11
12/// Table configuration for rendering and behavior
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct TableConfig {
15    pub title: Option<String>,
16    pub width: usize,
17    pub height: usize,
18    pub show_headers: bool,
19    pub sort_column: Option<String>,
20    pub sort_ascending: bool,
21    pub filters: HashMap<String, String>,
22    pub column_widths: Option<Vec<usize>>,
23    pub border_style: TableBorderStyle,
24    pub zebra_striping: bool,
25    pub highlight_row: Option<usize>,
26    pub max_column_width: Option<usize>,
27    pub show_row_numbers: bool,
28    pub pagination: Option<TablePagination>,
29}
30
31/// Border styles for tables
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33pub enum TableBorderStyle {
34    None,
35    Single,
36    Double,
37    Rounded,
38    Thick,
39    Custom {
40        horizontal: char,
41        vertical: char,
42        top_left: char,
43        top_right: char,
44        bottom_left: char,
45        bottom_right: char,
46        cross: char,
47    },
48}
49
50/// Pagination configuration
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct TablePagination {
53    pub page_size: usize,
54    pub current_page: usize,
55    pub show_page_info: bool,
56}
57
58/// Sort direction for columns
59#[derive(Debug, Clone, PartialEq)]
60pub enum SortDirection {
61    Ascending,
62    Descending,
63}
64
65/// Data type for column sorting
66#[derive(Debug, Clone)]
67pub enum ColumnType {
68    Text,
69    Number,
70    Date,
71    Boolean,
72}
73
74impl Default for TableConfig {
75    fn default() -> Self {
76        Self {
77            title: None,
78            width: 80,
79            height: 20,
80            show_headers: true,
81            sort_column: None,
82            sort_ascending: true,
83            filters: HashMap::new(),
84            column_widths: None,
85            border_style: TableBorderStyle::Single,
86            zebra_striping: false,
87            highlight_row: None,
88            max_column_width: Some(30),
89            show_row_numbers: false,
90            pagination: None,
91        }
92    }
93}
94
95/// Generate table display string from data and configuration
96pub fn render_table(data: &TableData, config: &TableConfig) -> String {
97    if data.headers.is_empty() || data.rows.is_empty() {
98        return "No data".to_string();
99    }
100
101    // Apply filters first
102    let filtered_rows = apply_filters(data, &config.filters);
103
104    // Apply sorting
105    let sorted_rows = apply_sorting(
106        &filtered_rows,
107        &data.headers,
108        &config.sort_column,
109        config.sort_ascending,
110    );
111
112    // Apply pagination
113    let (paginated_rows, page_info) = apply_pagination(&sorted_rows, &config.pagination);
114
115    // Calculate column widths
116    let column_widths = calculate_column_widths(data, config, &paginated_rows);
117
118    let mut result = String::new();
119
120    // Add title if present
121    if let Some(title) = &config.title {
122        result.push_str(&format!("{}\n", title));
123    }
124
125    // Render table structure
126    result.push_str(&render_top_border(&column_widths, &config.border_style));
127
128    // Render headers
129    if config.show_headers {
130        result.push_str(&render_header_row(
131            &data.headers,
132            &column_widths,
133            &config.border_style,
134            config.show_row_numbers,
135        ));
136        result.push_str(&render_separator(&column_widths, &config.border_style));
137    }
138
139    // Render data rows
140    for (index, row) in paginated_rows.iter().enumerate() {
141        let is_highlighted = config.highlight_row.map_or(false, |h| h == index);
142        let is_zebra = config.zebra_striping && index % 2 == 1;
143        result.push_str(&render_data_row(
144            row,
145            &column_widths,
146            &config.border_style,
147            config.show_row_numbers,
148            index,
149            is_highlighted,
150            is_zebra,
151        ));
152    }
153
154    // Render bottom border
155    result.push_str(&render_bottom_border(&column_widths, &config.border_style));
156
157    // Add pagination info
158    if let Some(info) = page_info {
159        result.push_str(&format!("\n{}", info));
160    }
161
162    result
163}
164
165/// Apply filters to table data
166fn apply_filters(data: &TableData, filters: &HashMap<String, String>) -> Vec<Vec<String>> {
167    if filters.is_empty() {
168        return data.rows.clone();
169    }
170
171    let mut filtered = Vec::new();
172
173    for row in &data.rows {
174        let mut matches = true;
175
176        for (column_name, filter_value) in filters {
177            if let Some(column_index) = data.headers.iter().position(|h| h == column_name) {
178                if column_index < row.len() {
179                    let cell_value = &row[column_index];
180                    if cell_value.to_lowercase() != filter_value.to_lowercase() {
181                        matches = false;
182                        break;
183                    }
184                }
185            }
186        }
187
188        if matches {
189            filtered.push(row.clone());
190        }
191    }
192
193    filtered
194}
195
196/// Apply sorting to table data
197fn apply_sorting(
198    rows: &[Vec<String>],
199    headers: &[String],
200    sort_column: &Option<String>,
201    ascending: bool,
202) -> Vec<Vec<String>> {
203    if let Some(column_name) = sort_column {
204        if let Some(column_index) = headers.iter().position(|h| h == column_name) {
205            let mut sorted = rows.to_vec();
206
207            sorted.sort_by(|a, b| {
208                let default_string = String::new();
209                let a_val = a.get(column_index).unwrap_or(&default_string);
210                let b_val = b.get(column_index).unwrap_or(&default_string);
211
212                // Try to parse as numbers first
213                if let (Ok(a_num), Ok(b_num)) = (a_val.parse::<f64>(), b_val.parse::<f64>()) {
214                    if ascending {
215                        a_num
216                            .partial_cmp(&b_num)
217                            .unwrap_or(std::cmp::Ordering::Equal)
218                    } else {
219                        b_num
220                            .partial_cmp(&a_num)
221                            .unwrap_or(std::cmp::Ordering::Equal)
222                    }
223                } else {
224                    // Fall back to string comparison
225                    if ascending {
226                        a_val.cmp(b_val)
227                    } else {
228                        b_val.cmp(a_val)
229                    }
230                }
231            });
232
233            return sorted;
234        }
235    }
236
237    rows.to_vec()
238}
239
240/// Apply pagination to table data
241fn apply_pagination(
242    rows: &[Vec<String>],
243    pagination: &Option<TablePagination>,
244) -> (Vec<Vec<String>>, Option<String>) {
245    if let Some(pag) = pagination {
246        let total_rows = rows.len();
247        let total_pages = (total_rows + pag.page_size - 1) / pag.page_size;
248        let start_index = pag.current_page * pag.page_size;
249        let end_index = std::cmp::min(start_index + pag.page_size, total_rows);
250
251        let paginated = if start_index < total_rows {
252            rows[start_index..end_index].to_vec()
253        } else {
254            Vec::new()
255        };
256
257        let page_info = if pag.show_page_info {
258            Some(format!(
259                "Page {} of {} ({} total rows)",
260                pag.current_page + 1,
261                total_pages,
262                total_rows
263            ))
264        } else {
265            None
266        };
267
268        (paginated, page_info)
269    } else {
270        (rows.to_vec(), None)
271    }
272}
273
274/// Calculate optimal column widths
275fn calculate_column_widths(
276    data: &TableData,
277    config: &TableConfig,
278    rows: &[Vec<String>],
279) -> Vec<usize> {
280    if let Some(widths) = &config.column_widths {
281        return widths.clone();
282    }
283
284    let mut widths = Vec::new();
285    let max_width = config.max_column_width.unwrap_or(30);
286
287    for (i, header) in data.headers.iter().enumerate() {
288        let mut max_len = header.len();
289
290        // Check all rows for this column
291        for row in rows {
292            if let Some(cell) = row.get(i) {
293                max_len = max_len.max(cell.len());
294            }
295        }
296
297        // Apply max width constraint
298        max_len = max_len.min(max_width);
299
300        // Minimum width of 3 for readability
301        max_len = max_len.max(3);
302
303        widths.push(max_len);
304    }
305
306    widths
307}
308
309/// Render top border of table
310fn render_top_border(widths: &[usize], style: &TableBorderStyle) -> String {
311    match style {
312        TableBorderStyle::None => String::new(),
313        TableBorderStyle::Single => {
314            let mut border = String::from("┌");
315            for (i, &width) in widths.iter().enumerate() {
316                border.push_str(&"─".repeat(width + 2));
317                if i < widths.len() - 1 {
318                    border.push('┬');
319                }
320            }
321            border.push('┐');
322            border.push('\n');
323            border
324        }
325        TableBorderStyle::Double => {
326            let mut border = String::from("╔");
327            for (i, &width) in widths.iter().enumerate() {
328                border.push_str(&"═".repeat(width + 2));
329                if i < widths.len() - 1 {
330                    border.push('╦');
331                }
332            }
333            border.push('╗');
334            border.push('\n');
335            border
336        }
337        TableBorderStyle::Rounded => {
338            let mut border = String::from("╭");
339            for (i, &width) in widths.iter().enumerate() {
340                border.push_str(&"─".repeat(width + 2));
341                if i < widths.len() - 1 {
342                    border.push('┬');
343                }
344            }
345            border.push('╮');
346            border.push('\n');
347            border
348        }
349        TableBorderStyle::Thick => {
350            let mut border = String::from("┏");
351            for (i, &width) in widths.iter().enumerate() {
352                border.push_str(&"━".repeat(width + 2));
353                if i < widths.len() - 1 {
354                    border.push('┳');
355                }
356            }
357            border.push('┓');
358            border.push('\n');
359            border
360        }
361        TableBorderStyle::Custom {
362            horizontal,
363            vertical: _,
364            top_left,
365            top_right,
366            bottom_left: _,
367            bottom_right: _,
368            cross,
369        } => {
370            let mut border = String::from(*top_left);
371            for (i, &width) in widths.iter().enumerate() {
372                border.push_str(&horizontal.to_string().repeat(width + 2));
373                if i < widths.len() - 1 {
374                    border.push(*cross);
375                }
376            }
377            border.push(*top_right);
378            border.push('\n');
379            border
380        }
381    }
382}
383
384/// Render bottom border of table
385fn render_bottom_border(widths: &[usize], style: &TableBorderStyle) -> String {
386    match style {
387        TableBorderStyle::None => String::new(),
388        TableBorderStyle::Single => {
389            let mut border = String::from("└");
390            for (i, &width) in widths.iter().enumerate() {
391                border.push_str(&"─".repeat(width + 2));
392                if i < widths.len() - 1 {
393                    border.push('┴');
394                }
395            }
396            border.push('┘');
397            border.push('\n');
398            border
399        }
400        TableBorderStyle::Double => {
401            let mut border = String::from("╚");
402            for (i, &width) in widths.iter().enumerate() {
403                border.push_str(&"═".repeat(width + 2));
404                if i < widths.len() - 1 {
405                    border.push('╩');
406                }
407            }
408            border.push('╝');
409            border.push('\n');
410            border
411        }
412        TableBorderStyle::Rounded => {
413            let mut border = String::from("╰");
414            for (i, &width) in widths.iter().enumerate() {
415                border.push_str(&"─".repeat(width + 2));
416                if i < widths.len() - 1 {
417                    border.push('┴');
418                }
419            }
420            border.push('╯');
421            border.push('\n');
422            border
423        }
424        TableBorderStyle::Thick => {
425            let mut border = String::from("┗");
426            for (i, &width) in widths.iter().enumerate() {
427                border.push_str(&"━".repeat(width + 2));
428                if i < widths.len() - 1 {
429                    border.push('┻');
430                }
431            }
432            border.push('┛');
433            border.push('\n');
434            border
435        }
436        TableBorderStyle::Custom {
437            horizontal,
438            vertical: _,
439            top_left: _,
440            top_right: _,
441            bottom_left,
442            bottom_right,
443            cross,
444        } => {
445            let mut border = String::from(*bottom_left);
446            for (i, &width) in widths.iter().enumerate() {
447                border.push_str(&horizontal.to_string().repeat(width + 2));
448                if i < widths.len() - 1 {
449                    border.push(*cross);
450                }
451            }
452            border.push(*bottom_right);
453            border.push('\n');
454            border
455        }
456    }
457}
458
459/// Render separator line between header and data
460fn render_separator(widths: &[usize], style: &TableBorderStyle) -> String {
461    match style {
462        TableBorderStyle::None => String::new(),
463        TableBorderStyle::Single => {
464            let mut sep = String::from("├");
465            for (i, &width) in widths.iter().enumerate() {
466                sep.push_str(&"─".repeat(width + 2));
467                if i < widths.len() - 1 {
468                    sep.push('┼');
469                }
470            }
471            sep.push('┤');
472            sep.push('\n');
473            sep
474        }
475        TableBorderStyle::Double => {
476            let mut sep = String::from("╠");
477            for (i, &width) in widths.iter().enumerate() {
478                sep.push_str(&"═".repeat(width + 2));
479                if i < widths.len() - 1 {
480                    sep.push('╬');
481                }
482            }
483            sep.push('╣');
484            sep.push('\n');
485            sep
486        }
487        TableBorderStyle::Rounded | TableBorderStyle::Thick => {
488            // Use single line separator for rounded and thick styles
489            let mut sep = String::from("├");
490            for (i, &width) in widths.iter().enumerate() {
491                sep.push_str(&"─".repeat(width + 2));
492                if i < widths.len() - 1 {
493                    sep.push('┼');
494                }
495            }
496            sep.push('┤');
497            sep.push('\n');
498            sep
499        }
500        TableBorderStyle::Custom {
501            horizontal,
502            vertical,
503            top_left: _,
504            top_right: _,
505            bottom_left: _,
506            bottom_right: _,
507            cross,
508        } => {
509            let mut sep = String::from(*vertical);
510            for (i, &width) in widths.iter().enumerate() {
511                sep.push_str(&horizontal.to_string().repeat(width + 2));
512                if i < widths.len() - 1 {
513                    sep.push(*cross);
514                }
515            }
516            sep.push(*vertical);
517            sep.push('\n');
518            sep
519        }
520    }
521}
522
523/// Render header row
524fn render_header_row(
525    headers: &[String],
526    widths: &[usize],
527    style: &TableBorderStyle,
528    show_row_numbers: bool,
529) -> String {
530    let vertical_char = match style {
531        TableBorderStyle::None => ' ',
532        TableBorderStyle::Single | TableBorderStyle::Rounded | TableBorderStyle::Thick => '│',
533        TableBorderStyle::Double => '║',
534        TableBorderStyle::Custom { vertical, .. } => *vertical,
535    };
536
537    let mut row = String::new();
538
539    if style != &TableBorderStyle::None {
540        row.push(vertical_char);
541    }
542
543    // Add row number column header if enabled
544    if show_row_numbers {
545        row.push_str(" # ");
546        if style != &TableBorderStyle::None {
547            row.push(vertical_char);
548        }
549    }
550
551    for (i, header) in headers.iter().enumerate() {
552        let width = widths[i];
553        let truncated = if header.len() > width {
554            format!("{}…", &header[..width.saturating_sub(1)])
555        } else {
556            header.clone()
557        };
558
559        row.push(' ');
560        row.push_str(&format!("{:width$}", truncated, width = width));
561        row.push(' ');
562
563        if i < headers.len() - 1 && style != &TableBorderStyle::None {
564            row.push(vertical_char);
565        }
566    }
567
568    if style != &TableBorderStyle::None {
569        row.push(vertical_char);
570    }
571
572    row.push('\n');
573    row
574}
575
576/// Render data row
577fn render_data_row(
578    row: &[String],
579    widths: &[usize],
580    style: &TableBorderStyle,
581    show_row_numbers: bool,
582    row_index: usize,
583    _is_highlighted: bool,
584    _is_zebra: bool,
585) -> String {
586    let vertical_char = match style {
587        TableBorderStyle::None => ' ',
588        TableBorderStyle::Single | TableBorderStyle::Rounded | TableBorderStyle::Thick => '│',
589        TableBorderStyle::Double => '║',
590        TableBorderStyle::Custom { vertical, .. } => *vertical,
591    };
592
593    let mut result = String::new();
594
595    if style != &TableBorderStyle::None {
596        result.push(vertical_char);
597    }
598
599    // Add row number if enabled
600    if show_row_numbers {
601        result.push_str(&format!(" {} ", row_index + 1));
602        if style != &TableBorderStyle::None {
603            result.push(vertical_char);
604        }
605    }
606
607    for (i, cell) in row.iter().enumerate() {
608        let width = widths.get(i).copied().unwrap_or(10);
609        let truncated = if cell.len() > width {
610            format!("{}…", &cell[..width.saturating_sub(1)])
611        } else {
612            cell.clone()
613        };
614
615        result.push(' ');
616        result.push_str(&format!("{:width$}", truncated, width = width));
617        result.push(' ');
618
619        if i < row.len() - 1 && style != &TableBorderStyle::None {
620            result.push(vertical_char);
621        }
622    }
623
624    if style != &TableBorderStyle::None {
625        result.push(vertical_char);
626    }
627
628    result.push('\n');
629    result
630}
631
632/// Parse table data from CSV-like text content
633pub fn parse_table_data(content: &str, delimiter: Option<char>) -> TableData {
634    let delimiter = delimiter.unwrap_or(',');
635    let lines: Vec<&str> = content.lines().collect();
636
637    if lines.is_empty() {
638        return TableData {
639            headers: Vec::new(),
640            rows: Vec::new(),
641            metadata: HashMap::new(),
642        };
643    }
644
645    // First line is headers
646    let headers: Vec<String> = lines[0]
647        .split(delimiter)
648        .map(|s| s.trim().to_string())
649        .collect();
650
651    // Remaining lines are data rows
652    let mut rows = Vec::new();
653    for line in lines.iter().skip(1) {
654        if line.trim().is_empty() {
655            continue;
656        }
657
658        let row: Vec<String> = line
659            .split(delimiter)
660            .map(|s| s.trim().to_string())
661            .collect();
662
663        rows.push(row);
664    }
665
666    let mut metadata = HashMap::new();
667    metadata.insert("source".to_string(), "parsed_csv".to_string());
668    metadata.insert("delimiter".to_string(), delimiter.to_string());
669    metadata.insert("rows_count".to_string(), rows.len().to_string());
670    metadata.insert("columns_count".to_string(), headers.len().to_string());
671
672    TableData {
673        headers,
674        rows,
675        metadata,
676    }
677}
678
679/// Parse table data from JSON content
680pub fn parse_table_data_from_json(content: &str) -> Result<TableData, serde_json::Error> {
681    // Try to parse as array of objects first
682    if let Ok(objects) = serde_json::from_str::<Vec<serde_json::Value>>(content) {
683        if objects.is_empty() {
684            return Ok(TableData {
685                headers: Vec::new(),
686                rows: Vec::new(),
687                metadata: HashMap::new(),
688            });
689        }
690
691        // Extract headers from first object
692        let mut headers = Vec::new();
693        if let Some(first_obj) = objects.first() {
694            if let serde_json::Value::Object(map) = first_obj {
695                for key in map.keys() {
696                    headers.push(key.clone());
697                }
698            }
699        }
700
701        // Extract rows
702        let mut rows = Vec::new();
703        for obj in &objects {
704            if let serde_json::Value::Object(map) = obj {
705                let mut row = Vec::new();
706                for header in &headers {
707                    let value = map
708                        .get(header)
709                        .map(|v| match v {
710                            serde_json::Value::String(s) => s.clone(),
711                            serde_json::Value::Number(n) => n.to_string(),
712                            serde_json::Value::Bool(b) => b.to_string(),
713                            serde_json::Value::Null => "".to_string(),
714                            _ => format!("{}", v),
715                        })
716                        .unwrap_or_default();
717                    row.push(value);
718                }
719                rows.push(row);
720            }
721        }
722
723        let mut metadata = HashMap::new();
724        metadata.insert("source".to_string(), "parsed_json".to_string());
725        metadata.insert("format".to_string(), "array_of_objects".to_string());
726        metadata.insert("rows_count".to_string(), rows.len().to_string());
727        metadata.insert("columns_count".to_string(), headers.len().to_string());
728
729        return Ok(TableData {
730            headers,
731            rows,
732            metadata,
733        });
734    }
735
736    // Try to parse as structured table data
737    serde_json::from_str(content)
738}
739
740#[cfg(test)]
741mod tests {
742    use super::*;
743
744    #[test]
745    fn test_parse_csv_data() {
746        let csv_content = "Name,Age,City\nJohn,25,New York\nJane,30,Los Angeles\nBob,35,Chicago";
747        let table = parse_table_data(csv_content, None);
748
749        assert_eq!(table.headers, vec!["Name", "Age", "City"]);
750        assert_eq!(table.rows.len(), 3);
751        assert_eq!(table.rows[0], vec!["John", "25", "New York"]);
752        assert_eq!(table.rows[2], vec!["Bob", "35", "Chicago"]);
753    }
754
755    #[test]
756    fn test_table_rendering() {
757        let data = TableData {
758            headers: vec!["ID".to_string(), "Name".to_string(), "Status".to_string()],
759            rows: vec![
760                vec!["1".to_string(), "Alice".to_string(), "Active".to_string()],
761                vec!["2".to_string(), "Bob".to_string(), "Inactive".to_string()],
762                vec!["3".to_string(), "Charlie".to_string(), "Active".to_string()],
763            ],
764            metadata: HashMap::new(),
765        };
766
767        let config = TableConfig::default();
768        let result = render_table(&data, &config);
769
770        assert!(result.contains("ID"));
771        assert!(result.contains("Alice"));
772        assert!(result.contains("┌"));
773        assert!(result.contains("└"));
774    }
775
776    #[test]
777    fn test_table_filtering() {
778        let data = TableData {
779            headers: vec!["Name".to_string(), "Status".to_string()],
780            rows: vec![
781                vec!["Alice".to_string(), "Active".to_string()],
782                vec!["Bob".to_string(), "Inactive".to_string()],
783                vec!["Charlie".to_string(), "Active".to_string()],
784            ],
785            metadata: HashMap::new(),
786        };
787
788        let mut filters = HashMap::new();
789        filters.insert("Status".to_string(), "Active".to_string());
790
791        let filtered = apply_filters(&data, &filters);
792        assert_eq!(filtered.len(), 2);
793        assert!(filtered.iter().all(|row| row[1] == "Active"));
794    }
795
796    #[test]
797    fn test_table_sorting() {
798        let rows = vec![
799            vec!["Charlie".to_string(), "30".to_string()],
800            vec!["Alice".to_string(), "25".to_string()],
801            vec!["Bob".to_string(), "35".to_string()],
802        ];
803        let headers = vec!["Name".to_string(), "Age".to_string()];
804
805        // Sort by name
806        let sorted = apply_sorting(&rows, &headers, &Some("Name".to_string()), true);
807        assert_eq!(sorted[0][0], "Alice");
808        assert_eq!(sorted[2][0], "Charlie");
809
810        // Sort by age (numeric)
811        let sorted = apply_sorting(&rows, &headers, &Some("Age".to_string()), true);
812        assert_eq!(sorted[0][1], "25");
813        assert_eq!(sorted[2][1], "35");
814    }
815
816    #[test]
817    fn test_json_parsing() {
818        let json_content = r#"[
819            {"id": 1, "name": "Alice", "active": true},
820            {"id": 2, "name": "Bob", "active": false}
821        ]"#;
822
823        let table = parse_table_data_from_json(json_content).unwrap();
824        assert_eq!(table.rows.len(), 2);
825        assert!(table.headers.contains(&"id".to_string()));
826        assert!(table.headers.contains(&"name".to_string()));
827    }
828
829    #[test]
830    fn test_pagination() {
831        let rows = vec![
832            vec!["1".to_string()],
833            vec!["2".to_string()],
834            vec!["3".to_string()],
835            vec!["4".to_string()],
836            vec!["5".to_string()],
837        ];
838
839        let pagination = TablePagination {
840            page_size: 2,
841            current_page: 1,
842            show_page_info: true,
843        };
844
845        let (paginated, info) = apply_pagination(&rows, &Some(pagination));
846        assert_eq!(paginated.len(), 2);
847        assert_eq!(paginated[0][0], "3");
848        assert_eq!(paginated[1][0], "4");
849        assert!(info.unwrap().contains("Page 2 of 3"));
850    }
851}