1use crate::llm::types::{DxLlmValue, DxSection};
21use std::collections::HashMap;
22
23#[derive(Debug, Clone)]
25pub struct TableWrapperConfig {
26 pub max_width: usize,
28 pub min_col_width: usize,
30 pub continuation_indicator: String,
32}
33
34impl Default for TableWrapperConfig {
35 fn default() -> Self {
36 Self {
37 max_width: 120,
38 min_col_width: 5,
39 continuation_indicator: "↓".to_string(),
40 }
41 }
42}
43
44impl TableWrapperConfig {
45 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn with_max_width(mut self, width: usize) -> Self {
52 self.max_width = width;
53 self
54 }
55
56 pub fn with_min_col_width(mut self, width: usize) -> Self {
58 self.min_col_width = width;
59 self
60 }
61}
62
63pub struct TableWrapper {
65 config: TableWrapperConfig,
66}
67
68impl TableWrapper {
69 pub fn new() -> Self {
71 Self {
72 config: TableWrapperConfig::default(),
73 }
74 }
75
76 pub fn with_config(config: TableWrapperConfig) -> Self {
78 Self { config }
79 }
80
81 pub fn needs_wrapping(&self, col_widths: &[usize]) -> bool {
83 let total_width = self.calculate_table_width(col_widths);
84 total_width > self.config.max_width
85 }
86
87 fn calculate_table_width(&self, col_widths: &[usize]) -> usize {
89 if col_widths.is_empty() {
94 return 0;
95 }
96 1 + col_widths.iter().map(|w| w + 3).sum::<usize>()
97 }
98
99 pub fn calculate_widths(
101 &self,
102 _section: &DxSection,
103 header_widths: &[usize],
104 cell_widths: &[Vec<usize>],
105 ) -> Vec<usize> {
106 let mut widths: Vec<usize> = header_widths.to_vec();
108
109 for row_widths in cell_widths {
110 for (i, &w) in row_widths.iter().enumerate() {
111 if i < widths.len() {
112 widths[i] = widths[i].max(w);
113 }
114 }
115 }
116
117 for w in &mut widths {
119 *w = (*w).max(self.config.min_col_width);
120 }
121
122 if !self.needs_wrapping(&widths) {
124 return widths;
125 }
126
127 let current_width = self.calculate_table_width(&widths);
129 let excess = current_width.saturating_sub(self.config.max_width);
130
131 if excess == 0 {
132 return widths;
133 }
134
135 let shrinkable: Vec<(usize, usize)> = widths
137 .iter()
138 .enumerate()
139 .filter(|&(_, &w)| w > self.config.min_col_width)
140 .map(|(i, &w)| (i, w - self.config.min_col_width))
141 .collect();
142
143 let total_shrinkable: usize = shrinkable.iter().map(|(_, s)| s).sum();
144
145 if total_shrinkable == 0 {
146 return widths; }
148
149 let mut remaining_excess = excess;
150 for (i, shrink_room) in shrinkable {
151 let shrink_amount = (shrink_room * excess / total_shrinkable).min(remaining_excess);
152 widths[i] = widths[i].saturating_sub(shrink_amount);
153 remaining_excess = remaining_excess.saturating_sub(shrink_amount);
154 }
155
156 widths
157 }
158
159 pub fn wrap_cell(&self, content: &str, max_width: usize) -> Vec<String> {
161 if content.chars().count() <= max_width {
162 return vec![content.to_string()];
163 }
164
165 let mut lines = Vec::new();
166 let mut current_line = String::new();
167 let mut char_count = 0;
168
169 for ch in content.chars() {
170 if char_count >= max_width {
171 lines.push(current_line);
172 current_line = String::new();
173 char_count = 0;
174 }
175 current_line.push(ch);
176 char_count += 1;
177 }
178
179 if !current_line.is_empty() {
180 lines.push(current_line);
181 }
182
183 lines
184 }
185
186 pub fn wrap_row(
188 &self,
189 row: &[DxLlmValue],
190 col_widths: &[usize],
191 refs: &HashMap<String, String>,
192 format_cell: impl Fn(&DxLlmValue, &HashMap<String, String>) -> String,
193 ) -> Vec<Vec<String>> {
194 let formatted_cells: Vec<String> = row.iter().map(|v| format_cell(v, refs)).collect();
196
197 let wrapped_cells: Vec<Vec<String>> = formatted_cells
199 .iter()
200 .enumerate()
201 .map(|(i, content)| {
202 let max_width = col_widths
203 .get(i)
204 .copied()
205 .unwrap_or(self.config.min_col_width);
206 self.wrap_cell(content, max_width)
207 })
208 .collect();
209
210 let max_lines = wrapped_cells.iter().map(|c| c.len()).max().unwrap_or(1);
212
213 let mut result = Vec::new();
215 for line_idx in 0..max_lines {
216 let mut line_cells = Vec::new();
217 for (col_idx, wrapped) in wrapped_cells.iter().enumerate() {
218 let cell_content = wrapped.get(line_idx).cloned().unwrap_or_default();
219 let width = col_widths
220 .get(col_idx)
221 .copied()
222 .unwrap_or(self.config.min_col_width);
223
224 let padding = width.saturating_sub(cell_content.chars().count());
226 let padded = format!("{}{}", cell_content, " ".repeat(padding));
227 line_cells.push(padded);
228 }
229 result.push(line_cells);
230 }
231
232 result
233 }
234
235 pub fn config(&self) -> &TableWrapperConfig {
237 &self.config
238 }
239}
240
241impl Default for TableWrapper {
242 fn default() -> Self {
243 Self::new()
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_needs_wrapping_small_table() {
253 let wrapper = TableWrapper::new();
254 let col_widths = vec![5, 10, 8];
255
256 assert!(!wrapper.needs_wrapping(&col_widths));
258 }
259
260 #[test]
261 fn test_needs_wrapping_wide_table() {
262 let config = TableWrapperConfig::new().with_max_width(50);
263 let wrapper = TableWrapper::with_config(config);
264
265 let col_widths = vec![20, 20, 20];
267 assert!(wrapper.needs_wrapping(&col_widths));
269 }
270
271 #[test]
272 fn test_wrap_cell_short_content() {
273 let wrapper = TableWrapper::new();
274 let result = wrapper.wrap_cell("hello", 10);
275
276 assert_eq!(result.len(), 1);
277 assert_eq!(result[0], "hello");
278 }
279
280 #[test]
281 fn test_wrap_cell_long_content() {
282 let wrapper = TableWrapper::new();
283 let result = wrapper.wrap_cell("hello world", 5);
284
285 assert_eq!(result.len(), 3);
286 assert_eq!(result[0], "hello");
287 assert_eq!(result[1], " worl");
288 assert_eq!(result[2], "d");
289 }
290
291 #[test]
292 fn test_wrap_cell_exact_width() {
293 let wrapper = TableWrapper::new();
294 let result = wrapper.wrap_cell("hello", 5);
295
296 assert_eq!(result.len(), 1);
297 assert_eq!(result[0], "hello");
298 }
299
300 #[test]
301 fn test_calculate_widths_no_shrink_needed() {
302 let wrapper = TableWrapper::new();
303 let section = DxSection::new(vec!["id".to_string(), "name".to_string()]);
304 let header_widths = vec![2, 4];
305 let cell_widths = vec![vec![1, 5], vec![2, 6]];
306
307 let result = wrapper.calculate_widths(§ion, &header_widths, &cell_widths);
308
309 assert_eq!(result[0], 5); assert_eq!(result[1], 6); }
313
314 #[test]
315 fn test_calculate_widths_with_shrink() {
316 let config = TableWrapperConfig::new()
317 .with_max_width(50)
318 .with_min_col_width(3);
319 let wrapper = TableWrapper::with_config(config);
320
321 let section = DxSection::new(vec!["col1".to_string(), "col2".to_string()]);
322 let header_widths = vec![20, 20];
323 let cell_widths = vec![vec![20, 20]];
324
325 let result = wrapper.calculate_widths(§ion, &header_widths, &cell_widths);
326
327 assert!(result[0] >= 3); assert!(result[1] >= 3); assert_eq!(result[0], 20);
336 assert_eq!(result[1], 20);
337 }
338
339 #[test]
340 fn test_wrap_row_no_wrap_needed() {
341 let wrapper = TableWrapper::new();
342 let row = vec![DxLlmValue::Num(1.0), DxLlmValue::Str("test".to_string())];
343 let col_widths = vec![5, 10];
344 let refs = HashMap::new();
345
346 let format_cell = |v: &DxLlmValue, _: &HashMap<String, String>| match v {
347 DxLlmValue::Num(n) => format!("{}", *n as i64),
348 DxLlmValue::Str(s) => s.clone(),
349 _ => String::new(),
350 };
351
352 let result = wrapper.wrap_row(&row, &col_widths, &refs, format_cell);
353
354 assert_eq!(result.len(), 1);
355 assert_eq!(result[0].len(), 2);
356 }
357
358 #[test]
359 fn test_wrap_row_with_wrap() {
360 let wrapper = TableWrapper::new();
361 let row = vec![
362 DxLlmValue::Num(1.0),
363 DxLlmValue::Str("this is a long string".to_string()),
364 ];
365 let col_widths = vec![5, 10];
366 let refs = HashMap::new();
367
368 let format_cell = |v: &DxLlmValue, _: &HashMap<String, String>| match v {
369 DxLlmValue::Num(n) => format!("{}", *n as i64),
370 DxLlmValue::Str(s) => s.clone(),
371 _ => String::new(),
372 };
373
374 let result = wrapper.wrap_row(&row, &col_widths, &refs, format_cell);
375
376 assert!(result.len() > 1);
378
379 for (i, line) in result.iter().enumerate() {
381 if i > 0 {
382 assert!(line[0].trim().is_empty() || line[0].chars().count() <= col_widths[0]);
384 }
385 }
386 }
387
388 #[test]
389 fn test_table_width_calculation() {
390 let wrapper = TableWrapper::new();
391
392 assert_eq!(wrapper.calculate_table_width(&[]), 0);
394
395 assert_eq!(wrapper.calculate_table_width(&[5]), 9);
397
398 assert_eq!(wrapper.calculate_table_width(&[3, 4]), 14);
400 }
401}