format_tools 0.6.0

Collection of mechanisms for formatting and serialization into string.
Documentation
//! Implement classic table output format.
//!
//! # Example
//!
//! ```text
//!  sid | sname | gap
//! -----+-------+-----
//!    3 | Alice |   5
//!    6 | Joe   |   1
//!   10 | Boris |   5
//! ```

use crate::*;
use print::
{
  InputExtract,
  Context,
};
use core::fmt;
use std::sync::OnceLock;

/// A struct representing the classic table output format.
///
/// `Table` provides a standard implementation for table formatting,
/// supporting a classic style with default settings.
///
/// # Example
///
/// ```text
///  sid | sname | gap
/// -----+-------+-----
///    3 | Alice |   5
///    6 | Joe   |   1
///   10 | Boris |   5
/// ```
#[ derive( Debug ) ]
pub struct Table
{
  /// Delimitting header with grid line or not.
  pub delimitting_header : bool,
  /// Prefix added to each cell.
  pub cell_prefix : String,
  /// Postfix added to each cell.
  pub cell_postfix : String,
  /// Separator used between table columns.
  pub cell_separator : String,
  /// Prefix added to each row.
  pub row_prefix : String,
  /// Postfix added to each row.
  pub row_postfix : String,
  /// Separator used between rows.
  pub row_separator : String,
  /// Horizontal line character.
  pub h : char,
  /// Vertical line character.
  pub v : char,
  /// Left T-junction character.
  pub t_l : char,
  /// Right T-junction character.
  pub t_r : char,
  /// Top T-junction character.
  pub t_t : char,
  /// Bottom T-junction character.
  pub t_b : char,
  /// Cross junction character.
  pub cross : char,
  /// Top-left corner character.
  pub corner_lt : char,
  /// Top-right corner character.
  pub corner_rt : char,
  /// Bottom-left corner character.
  pub corner_lb : char,
  /// Bottom-right corner character.
  pub corner_rb : char,
  /// Limit table width. If the value is zero, then no limitation.
  pub max_width: usize,
}

impl Default for Table
{
  fn default() -> Self
  {

    let delimitting_header = true;

    let cell_prefix = "".to_string();
    let cell_postfix = "".to_string();
    let cell_separator = "".to_string();
    let row_prefix = "".to_string();
    let row_postfix = "".to_string();
    let row_separator = "\n".to_string();

    let h = '';
    let v = '|';
    let t_l = '';
    let t_r = '';
    let t_t = '';
    let t_b = '';
    let cross = '';
    let corner_lt = '';
    let corner_rt = '';
    let corner_lb = '';
    let corner_rb = '';
    let max_width = 0;

    Self
    {
      delimitting_header,
      cell_prefix,
      cell_postfix,
      cell_separator,
      row_prefix,
      row_postfix,
      row_separator,
      h,
      v,
      t_l,
      t_r,
      t_t,
      t_b,
      cross,
      corner_lt,
      corner_rt,
      corner_lb,
      corner_rb,
      max_width
    }
  }
}

impl Default for &'static Table
{
  fn default() -> Self
  {
    // qqq : find a better solution
    static STYLES : OnceLock< Table > = OnceLock::new();
    STYLES.get_or_init( ||
    {
      Table::default()
    })
  }
}

impl Table
{

  /// Returns a reference to a static instance of `Table`.
  ///
  /// This method provides access to a single shared instance of `Table`,
  /// ensuring efficient reuse of the classic table output format.
  pub fn instance() -> & 'static dyn TableOutputFormat
  {

    static INSTANCE : OnceLock< Table > = OnceLock::new();
    INSTANCE.get_or_init( ||
    {
      Self::default()
    })

  }

  /// Calculate how much space is minimally needed in order to generate a table output with the specified
  /// number of columns. It will be impossible to render table smaller than the result of
  /// `min_width()`.
  ///
  /// This function is similar to `output_format::Records::min_width`, but it contains a `column_count`
  /// parameter, and it aslo uses the `output_format::Table` style parameters.
  pub fn min_width
  (
    &self,
    column_count : usize,
  ) -> usize
  {
    self.row_prefix.chars().count()
    + self.row_postfix.chars().count()
    + column_count * ( self.cell_postfix.chars().count() + self.cell_prefix.chars().count() )
    + if column_count == 0 { 0 } else { ( column_count - 1 ) * self.cell_separator.chars().count() }
    + column_count
  }
}

impl TableOutputFormat for Table
{
  fn extract_write< 'buf, 'data >( &self, x : &InputExtract< 'data >, c : &mut Context< 'buf > ) -> fmt::Result
  {
    use format::text_wrap::text_wrap;

    let cell_prefix = &self.cell_prefix;
    let cell_postfix = &self.cell_postfix;
    let cell_separator = &self.cell_separator;
    let row_prefix = &self.row_prefix;
    let row_postfix = &self.row_postfix;
    let row_separator = &self.row_separator;
    let h = self.h.to_string();

    let column_count = x.col_descriptors.len();

    if self.max_width != 0 && ( self.min_width( column_count ) > self.max_width )
    {
      return Err( fmt::Error );
    }

    let columns_nowrap_width = x.col_descriptors.iter().map( |c| c.width ).sum::<usize>();
    let visual_elements_width = self.min_width( column_count ) - column_count;
    
    let filtered_data = x.row_descriptors.iter().filter_map( | r | 
    {
      if r.vis
      {
        Some( &x.data[ r.irow ] )
      }
      else
      {
        None
      }
    });
    
    let wrapped_text = text_wrap
    (
      filtered_data,
      x.col_descriptors.iter().map( | c | c.width ).collect::< Vec<  usize  > >(),
      if self.max_width == 0 { 0 } else { self.max_width - visual_elements_width }, 
      columns_nowrap_width 
    );

    let new_columns_widthes = wrapped_text.column_widthes.iter().sum::<usize>();
    let new_row_width = new_columns_widthes + visual_elements_width;

    let mut printed_row_count = 0;

    for row in wrapped_text.data.iter()
    {
      if printed_row_count == wrapped_text.first_row_height && x.has_header && self.delimitting_header
      {
        write!( c.buf, "{}", row_separator )?;
        write!( c.buf, "{}", h.repeat( new_row_width ) )?;
      }
      
      if printed_row_count > 0
      {
        write!( c.buf, "{}", row_separator )?;
      }

      printed_row_count += 1;

      write!( c.buf, "{}", row_prefix )?;

      for ( icol, col ) in row.iter().enumerate()
      {
        let cell_wrapped_width = col.wrap_width;
        let column_width = wrapped_text.column_widthes[ icol ];
        let slice_width = col.content.chars().count();
        
        if icol > 0
        {
          write!( c.buf, "{}", cell_separator )?;
        }

        write!( c.buf, "{}", cell_prefix )?;
        
        let lspaces = column_width.saturating_sub( cell_wrapped_width ) / 2;
        let rspaces = ( ( column_width.saturating_sub( cell_wrapped_width ) as f32 / 2 as f32 ) ).round() as usize + cell_wrapped_width.saturating_sub(slice_width);

        if lspaces > 0
        {
          write!( c.buf, "{:<width$}", " ", width = lspaces )?;
        }
        
        write!( c.buf, "{}", col.content )?;

        if rspaces > 0
        {
          write!( c.buf, "{:>width$}", " ", width = rspaces )?;
        }

        write!( c.buf, "{}", cell_postfix )?;
      }

      write!( c.buf, "{}", row_postfix )?;
    }

    Ok(())
  }
}