format_tools/format/output_format/
table.rs

1//! Implement classic table output format.
2//!
3//! # Example
4//!
5//! ```text
6//!  sid | sname | gap
7//! -----+-------+-----
8//!    3 | Alice |   5
9//!    6 | Joe   |   1
10//!   10 | Boris |   5
11//! ```
12
13use crate::*;
14use print::
15{
16  InputExtract,
17  Context,
18};
19use core::fmt;
20use std::sync::OnceLock;
21
22/// A struct representing the classic table output format.
23///
24/// `Table` provides a standard implementation for table formatting,
25/// supporting a classic style with default settings.
26///
27/// # Example
28///
29/// ```text
30///  sid | sname | gap
31/// -----+-------+-----
32///    3 | Alice |   5
33///    6 | Joe   |   1
34///   10 | Boris |   5
35/// ```
36#[ derive( Debug ) ]
37pub struct Table
38{
39  /// Delimitting header with grid line or not.
40  pub delimitting_header : bool,
41  /// Prefix added to each cell.
42  pub cell_prefix : String,
43  /// Postfix added to each cell.
44  pub cell_postfix : String,
45  /// Separator used between table columns.
46  pub cell_separator : String,
47  /// Prefix added to each row.
48  pub row_prefix : String,
49  /// Postfix added to each row.
50  pub row_postfix : String,
51  /// Separator used between rows.
52  pub row_separator : String,
53  /// Horizontal line character.
54  pub h : char,
55  /// Vertical line character.
56  pub v : char,
57  /// Left T-junction character.
58  pub t_l : char,
59  /// Right T-junction character.
60  pub t_r : char,
61  /// Top T-junction character.
62  pub t_t : char,
63  /// Bottom T-junction character.
64  pub t_b : char,
65  /// Cross junction character.
66  pub cross : char,
67  /// Top-left corner character.
68  pub corner_lt : char,
69  /// Top-right corner character.
70  pub corner_rt : char,
71  /// Bottom-left corner character.
72  pub corner_lb : char,
73  /// Bottom-right corner character.
74  pub corner_rb : char,
75  /// Limit table width. If the value is zero, then no limitation.
76  pub max_width: usize,
77}
78
79impl Default for Table
80{
81  fn default() -> Self
82  {
83
84    let delimitting_header = true;
85
86    let cell_prefix = "".to_string();
87    let cell_postfix = "".to_string();
88    let cell_separator = " │ ".to_string();
89    let row_prefix = "│ ".to_string();
90    let row_postfix = " │".to_string();
91    let row_separator = "\n".to_string();
92
93    let h = '─';
94    let v = '|';
95    let t_l = '├';
96    let t_r = '┤';
97    let t_t = '┬';
98    let t_b = '┴';
99    let cross = '┼';
100    let corner_lt = '┌';
101    let corner_rt = '┐';
102    let corner_lb = '└';
103    let corner_rb = '┘';
104    let max_width = 0;
105
106    Self
107    {
108      delimitting_header,
109      cell_prefix,
110      cell_postfix,
111      cell_separator,
112      row_prefix,
113      row_postfix,
114      row_separator,
115      h,
116      v,
117      t_l,
118      t_r,
119      t_t,
120      t_b,
121      cross,
122      corner_lt,
123      corner_rt,
124      corner_lb,
125      corner_rb,
126      max_width
127    }
128  }
129}
130
131impl Default for &'static Table
132{
133  fn default() -> Self
134  {
135    // qqq : find a better solution
136    static STYLES : OnceLock< Table > = OnceLock::new();
137    STYLES.get_or_init( ||
138    {
139      Table::default()
140    })
141  }
142}
143
144impl Table
145{
146
147  /// Returns a reference to a static instance of `Table`.
148  ///
149  /// This method provides access to a single shared instance of `Table`,
150  /// ensuring efficient reuse of the classic table output format.
151  pub fn instance() -> & 'static dyn TableOutputFormat
152  {
153
154    static INSTANCE : OnceLock< Table > = OnceLock::new();
155    INSTANCE.get_or_init( ||
156    {
157      Self::default()
158    })
159
160  }
161
162  /// Calculate how much space is minimally needed in order to generate a table output with the specified
163  /// number of columns. It will be impossible to render table smaller than the result of
164  /// `min_width()`.
165  ///
166  /// This function is similar to `output_format::Records::min_width`, but it contains a `column_count`
167  /// parameter, and it aslo uses the `output_format::Table` style parameters.
168  pub fn min_width
169  (
170    &self,
171    column_count : usize,
172  ) -> usize
173  {
174    self.row_prefix.chars().count()
175    + self.row_postfix.chars().count()
176    + column_count * ( self.cell_postfix.chars().count() + self.cell_prefix.chars().count() )
177    + if column_count == 0 { 0 } else { ( column_count - 1 ) * self.cell_separator.chars().count() }
178    + column_count
179  }
180}
181
182impl TableOutputFormat for Table
183{
184  fn extract_write< 'buf, 'data >( &self, x : &InputExtract< 'data >, c : &mut Context< 'buf > ) -> fmt::Result
185  {
186    use format::text_wrap::text_wrap;
187
188    let cell_prefix = &self.cell_prefix;
189    let cell_postfix = &self.cell_postfix;
190    let cell_separator = &self.cell_separator;
191    let row_prefix = &self.row_prefix;
192    let row_postfix = &self.row_postfix;
193    let row_separator = &self.row_separator;
194    let h = self.h.to_string();
195
196    let column_count = x.col_descriptors.len();
197
198    if self.max_width != 0 && ( self.min_width( column_count ) > self.max_width )
199    {
200      return Err( fmt::Error );
201    }
202
203    let columns_nowrap_width = x.col_descriptors.iter().map( |c| c.width ).sum::<usize>();
204    let visual_elements_width = self.min_width( column_count ) - column_count;
205    
206    let filtered_data = x.row_descriptors.iter().filter_map( | r | 
207    {
208      if r.vis
209      {
210        Some( &x.data[ r.irow ] )
211      }
212      else
213      {
214        None
215      }
216    });
217    
218    let wrapped_text = text_wrap
219    (
220      filtered_data,
221      x.col_descriptors.iter().map( | c | c.width ).collect::< Vec<  usize  > >(),
222      if self.max_width == 0 { 0 } else { self.max_width - visual_elements_width }, 
223      columns_nowrap_width 
224    );
225
226    let new_columns_widthes = wrapped_text.column_widthes.iter().sum::<usize>();
227    let new_row_width = new_columns_widthes + visual_elements_width;
228
229    let mut printed_row_count = 0;
230
231    for row in wrapped_text.data.iter()
232    {
233      if printed_row_count == wrapped_text.first_row_height && x.has_header && self.delimitting_header
234      {
235        write!( c.buf, "{}", row_separator )?;
236        write!( c.buf, "{}", h.repeat( new_row_width ) )?;
237      }
238      
239      if printed_row_count > 0
240      {
241        write!( c.buf, "{}", row_separator )?;
242      }
243
244      printed_row_count += 1;
245
246      write!( c.buf, "{}", row_prefix )?;
247
248      for ( icol, col ) in row.iter().enumerate()
249      {
250        let cell_wrapped_width = col.wrap_width;
251        let column_width = wrapped_text.column_widthes[ icol ];
252        let slice_width = col.content.chars().count();
253        
254        if icol > 0
255        {
256          write!( c.buf, "{}", cell_separator )?;
257        }
258
259        write!( c.buf, "{}", cell_prefix )?;
260        
261        let lspaces = column_width.saturating_sub( cell_wrapped_width ) / 2;
262        let rspaces = ( ( column_width.saturating_sub( cell_wrapped_width ) as f32 / 2 as f32 ) ).round() as usize + cell_wrapped_width.saturating_sub(slice_width);
263
264        if lspaces > 0
265        {
266          write!( c.buf, "{:<width$}", " ", width = lspaces )?;
267        }
268        
269        write!( c.buf, "{}", col.content )?;
270
271        if rspaces > 0
272        {
273          write!( c.buf, "{:>width$}", " ", width = rspaces )?;
274        }
275
276        write!( c.buf, "{}", cell_postfix )?;
277      }
278
279      write!( c.buf, "{}", row_postfix )?;
280    }
281
282    Ok(())
283  }
284}