diff --git a/module/core/format_tools/src/format.rs b/module/core/format_tools/src/format.rs index fb207181b3..6200a4f5d8 100644 --- a/module/core/format_tools/src/format.rs +++ b/module/core/format_tools/src/format.rs @@ -289,6 +289,7 @@ pub mod string; pub mod table; pub mod to_string; pub mod to_string_with_fallback; +pub mod text_wrap; /// A strucutre for diagnostic and demonstration purpose. #[ doc( hidden ) ] @@ -317,6 +318,7 @@ pub mod own table::orphan::*, to_string::orphan::*, to_string_with_fallback::orphan::*, + text_wrap::orphan::*, }; } @@ -369,6 +371,7 @@ pub mod exposed table::exposed::*, to_string::exposed::*, to_string_with_fallback::exposed::*, + text_wrap::exposed::*, }; } @@ -391,6 +394,7 @@ pub mod prelude table::prelude::*, to_string::prelude::*, to_string_with_fallback::prelude::*, + text_wrap::prelude::*, }; } diff --git a/module/core/format_tools/src/format/output_format/records.rs b/module/core/format_tools/src/format/output_format/records.rs index 45a1206e41..1c89f34038 100644 --- a/module/core/format_tools/src/format/output_format/records.rs +++ b/module/core/format_tools/src/format/output_format/records.rs @@ -22,12 +22,12 @@ //! use crate::*; -use md_math::MdOffset; use print:: { InputExtract, Context, }; +use std::borrow::Cow; use core:: { fmt, @@ -59,6 +59,8 @@ pub struct Records pub cell_postfix : String, /// Separator used between table columns. pub cell_separator : String, + /// Limit table width. If the value is zero, then no limitation. + pub max_width: usize, // /// Horizontal line character. // pub h : char, // /// Vertical line character. @@ -91,6 +93,25 @@ impl Records static INSTANCE : OnceLock< Records > = OnceLock::new(); INSTANCE.get_or_init( || Records::default() ) } + + /// Calculate how much space is minimally needed in order to generate an output with this output formatter. + /// It will be impossible to render tables smaller than the result of `min_width()`. + /// + /// This function is similar to `output_format::Table::min_width`, but it does not contain a + /// `column_count` as it always equal to 2, and it aslo uses the `output_format::Records` + /// style parameters. + pub fn min_width + ( + &self, + ) -> usize + { + // 2 is used here, because `Records` displays 2 columns: keys and values. + self.row_prefix.chars().count() + + self.row_postfix.chars().count() + + 2 * ( self.cell_postfix.chars().count() + self.cell_prefix.chars().count() ) + + self.cell_separator.chars().count() + + 2 + } } impl Default for Records @@ -108,6 +129,8 @@ impl Default for Records let table_postfix = "".to_string(); let table_separator = "\n".to_string(); + let max_width = 0; + // let h = '─'; // let v = '|'; // let t_l = '├'; @@ -131,6 +154,7 @@ impl Default for Records cell_prefix, cell_postfix, cell_separator, + max_width, // h, // v, // t_l, @@ -155,70 +179,88 @@ impl TableOutputFormat for Records c : & mut Context< 'buf >, ) -> fmt::Result { + use format::text_wrap::{ text_wrap, width_calculate }; + + if self.max_width != 0 && self.max_width < self.min_width() + { + return Err( fmt::Error ); + } + + // 2 because there are only 2 columns: key and value. + let columns_max_width = if self.max_width == 0 { 0 } else { self.max_width - self.min_width() + 2 }; - let label_width = x.header().fold( 0, | acc, cell | acc.max( cell.1[ 0 ] ) ); + let keys : Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > = x.header().collect(); + let keys_width = width_calculate( &keys ); write!( c.buf, "{}", self.table_prefix )?; - let mut first = true; - // Write each record - for ( irow, row ) in x.rows() - { + let mut printed_tables_count = 0; - if !row.vis + for ( itable_descriptor, table_descriptor ) in x.row_descriptors.iter().enumerate() + { + if !table_descriptor.vis || ( x.has_header && itable_descriptor == 0 ) { continue; } - if first - { - first = false; - } - else + if printed_tables_count > 0 { write!( c.buf, "{}", self.table_separator )?; } - let slice_width = x.data[ irow ].iter().fold( 0, | acc, cell | acc.max( cell.1[ 0 ] ) ); + printed_tables_count += 1; + + writeln!( c.buf, " = {}", table_descriptor.irow )?; - writeln!( c.buf, " = {}", irow )?; + let values = &x.data[ itable_descriptor ]; + let values_width = width_calculate( &values ); - for ( icol, _col ) in x.col_descriptors.iter().enumerate() + let table_for_wrapping : Vec< Vec< ( Cow< 'data, str >, [ usize; 2] ) > > = + keys.iter().enumerate().map( | ( ikey, key ) | { - let cell = &x.data[ irow ][ icol ]; - let height = cell.1[ 1 ]; + vec![ key.clone(), values[ ikey ].clone() ] + }).collect(); - for islice in 0..height + let wrapped_text = text_wrap + ( + table_for_wrapping.iter(), + &[ keys_width, values_width ], + columns_max_width, + keys_width + values_width, + ); + + for ( irow, cols ) in wrapped_text.data.into_iter().enumerate() + { + if irow != 0 { - let label = x.header_slice( islice, icol ); - let md_index = [ islice, icol, irow ]; - let slice = x.slices[ x.slices_dim.md_offset( md_index ) ]; - - if icol > 0 || islice > 0 - { - write!( c.buf, "{}", self.row_separator )?; - } - - write!( c.buf, "{}", self.row_prefix )?; - - write!( c.buf, "{}", self.cell_prefix )?; - write!( c.buf, "{: 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 md_math::MdOffset; + use format::text_wrap::text_wrap; let cell_prefix = &self.cell_prefix; let cell_postfix = &self.cell_postfix; @@ -173,105 +196,92 @@ impl TableOutputFormat for Table let row_separator = &self.row_separator; let h = self.h.to_string(); - let mut delimitting_header = self.delimitting_header; - let row_width = if delimitting_header + let column_count = x.col_descriptors.len(); + + if self.max_width != 0 && ( self.min_width( column_count ) > self.max_width ) { - let mut grid_width = x.mcells_vis[ 0 ] * ( cell_prefix.chars().count() + cell_postfix.chars().count() ); - grid_width += row_prefix.chars().count() + row_postfix.chars().count(); - if x.mcells_vis[ 0 ] > 0 - { - grid_width += ( x.mcells_vis[ 0 ] - 1 ) * ( cell_separator.chars().count() ); - } - x.mchars[ 0 ] + grid_width + return Err( fmt::Error ); } - else - { - 0 - }; - let mut prev_typ : Option< LineType > = None; - // dbg!( x.row_descriptors.len() ); - - for ( irow, row ) in x.row_descriptors.iter().enumerate() + let columns_nowrap_width = x.col_descriptors.iter().map( |c| c.width ).sum::(); + let visual_elements_width = self.min_width( column_count ) - column_count; + + let filtered_data = x.row_descriptors.iter().filter_map( | r | { - let height = row.height; - - if delimitting_header + if r.vis { - if let Some( prev_typ ) = prev_typ - { - if prev_typ == LineType::Header && row.typ == LineType::Regular - { - write!( c.buf, "{}", row_separator )?; - write!( c.buf, "{}", h.repeat( row_width ) )?; - delimitting_header = false - } - } - if row.vis - { - prev_typ = Some( row.typ ); - } + Some( &x.data[ r.irow ] ) } - - if !row.vis + else { - continue; + 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::(); + 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 )?; } - // dbg!( row.height ); + printed_row_count += 1; - for islice in 0..height - { + write!( c.buf, "{}", row_prefix )?; - if irow > 0 + 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, "{}", row_separator )?; + write!( c.buf, "{}", cell_separator )?; } - write!( c.buf, "{}", row_prefix )?; + write!( c.buf, "{}", cell_prefix )?; + + let lspaces = ( column_width - cell_wrapped_width ) / 2; + let rspaces = ( ( column_width - cell_wrapped_width ) as f32 / 2 as f32 ).round() as usize + cell_wrapped_width - slice_width; - for icol in 0 .. x.col_descriptors.len() + if lspaces > 0 { - let col = &x.col_descriptors[ icol ]; - let cell_width = x.data[ irow ][ icol ].1[0]; - let width = col.width; - let md_index = [ islice, icol, irow as usize ]; - let slice = x.slices[ x.slices_dim.md_offset( md_index ) ]; - - // println!( "md_index : {md_index:?} | md_offset : {} | slice : {slice}", x.slices_dim.md_offset( md_index ) ); - - if icol > 0 - { - write!( c.buf, "{}", cell_separator )?; - } - - write!( c.buf, "{}", cell_prefix )?; - - // println!( "icol : {icol} | irow : {irow} | width : {width} | cell_width : {cell_width} | slice.len() : {}", slice.len() ); - - let lspaces = ( width - cell_width ) / 2; - let rspaces = ( width - cell_width + 1 ) / 2 + cell_width - slice.chars().count(); - - // println!( "icol : {icol} | irow : {irow} | width : {width} | cell_width : {cell_width} | lspaces : {lspaces} | rspaces : {rspaces}" ); - - if lspaces > 0 - { - write!( c.buf, "{: 0 - { - write!( c.buf, "{:>width$}", " ", width = rspaces )?; - } + write!( c.buf, "{: 0 + { + write!( c.buf, "{:>width$}", " ", width = rspaces )?; } - write!( c.buf, "{}", row_postfix )?; + write!( c.buf, "{}", cell_postfix )?; } + write!( c.buf, "{}", row_postfix )?; } Ok(()) } -} +} \ No newline at end of file diff --git a/module/core/format_tools/src/format/print.rs b/module/core/format_tools/src/format/print.rs index f3c5d2acc6..aada1e5425 100644 --- a/module/core/format_tools/src/format/print.rs +++ b/module/core/format_tools/src/format/print.rs @@ -7,10 +7,9 @@ mod private { use crate::*; - use md_math::MdOffset; use std:: { - borrow::Cow, + borrow::{ Cow, Borrow }, collections::HashMap, }; use core:: @@ -282,11 +281,6 @@ mod private // string, size, pub data : Vec< Vec< ( Cow< 'data, str >, [ usize ; 2 ] ) > >, // xxx : use maybe flat vector - /// Dimensions of slices for retrieving data from multi-matrix. - pub slices_dim : [ usize ; 3 ], - /// Extracted slices or strings for further processing. - pub slices : Vec< &'data str >, - } // @@ -340,30 +334,26 @@ mod private /// Returns a slice from the header, or an empty string if no header is present. /// - /// This function retrieves a specific slice from the header row based on the provided indices. - /// If the table does not have a header, it returns an empty string. - /// /// # Arguments /// - /// - `islice`: The slice index within the header cell. /// - `icol`: The column index within the header row. /// /// # Returns /// - /// A string slice representing the header content at the specified indices. + /// A string slice representing the header content. /// - pub fn header_slice( & self, islice : usize, icol : usize ) -> & str + pub fn header_slice( & self, icol : usize ) -> & str { if self.has_header { - let md_index = [ islice, icol, 0 ]; - self.slices[ self.slices_dim.md_offset( md_index ) ] + self.data[ 0 ][ icol ].0.borrow() } else { "" } } + /// Extract input data from and collect it in a format consumable by output formatter. pub fn extract< 't, 'context, Table, RowKey, Row, CellKey> ( @@ -379,12 +369,10 @@ mod private Table : TableRows< RowKey = RowKey, Row = Row, CellKey = CellKey >, Table : TableHeader< CellKey = CellKey >, RowKey : table::RowKey, - Row : Cells< CellKey> + 'data, + Row : Cells< CellKey > + 'data, CellKey : table::CellKey + ?Sized + 'data, // CellRepr : table::CellRepr, { - use md_math::MdOffset; - // let mcells = table.mcells(); let mut mcells_vis = [ 0 ; 2 ]; let mut mcells = [ 0 ; 2 ]; @@ -532,22 +520,6 @@ mod private mchars[ 0 ] = col_descriptors.iter().fold( 0, | acc, col | acc + col.width ); mchars[ 1 ] = row_descriptors.iter().fold( 0, | acc, row | acc + if row.vis { row.height } else { 0 } ); - // cook slices multi-matrix - - let mut slices_dim = [ 1, mcells[ 0 ], mcells[ 1 ] ]; - slices_dim[ 0 ] = row_descriptors - .iter() - .fold( 0, | acc : usize, row | acc.max( row.height ) ) - ; - - let slices_len = slices_dim[ 0 ] * slices_dim[ 1 ] * slices_dim[ 2 ]; - let slices : Vec< &str > = vec![ "" ; slices_len ]; - - // assert_eq!( mcells, mcells, r#"Incorrect multidimensional size of table - // mcells <> mcells - // {mcells:?} <> {mcells:?}"# ); - // println!( "mcells : {mcells:?} | mcells : {mcells:?} | mcells_vis : {mcells_vis:?}" ); - let mut x = InputExtract::< '_ > { mcells, @@ -557,42 +529,16 @@ mod private row_descriptors, data, has_header, - slices_dim, - slices, }; - // extract slices - - let mut slices : Vec< &str > = vec![]; - std::mem::swap( &mut x.slices, &mut slices ); - - let mut irow : isize = -1; - for row_data in x.data.iter() + if x.data.len() > 0 { - - irow += 1; - for icol in 0 .. x.col_descriptors.len() { - let cell = &row_data[ icol ]; - string::lines( cell.0.as_ref() ) - .enumerate() - .for_each( | ( layer, s ) | - { - let md_index = [ layer, icol, irow as usize ]; - slices[ x.slices_dim.md_offset( md_index ) ] = s; - }) - ; - if irow == 0 - { - x.col_descriptors[ icol ].label = cell.0.as_ref(); - } + x.col_descriptors[ icol ].label = x.data[ 0 ][ icol ].0.as_ref(); } - } - std::mem::swap( &mut x.slices, &mut slices ); - return callback( &x ); } diff --git a/module/core/format_tools/src/format/string.rs b/module/core/format_tools/src/format/string.rs index 619d1690c2..ee34e9e718 100644 --- a/module/core/format_tools/src/format/string.rs +++ b/module/core/format_tools/src/format/string.rs @@ -114,6 +114,47 @@ mod private Lines::new( src.as_ref() ) } + /// Returns an iterator over the lines of a string slice with text wrapping. + /// + /// This function provides an iterator that yields each line of the input string slice. + /// It is based on previous iterator `lines` but it also includes text wrapping that is + /// controlled via `limit_width` argument. If the string contains a trailing new line, + /// then an empty string will be yielded in this iterator. + /// + /// # Arguments + /// + /// * `src` - A reference to a type that can be converted to a string slice. This allows + /// for flexibility in passing various string-like types. + /// + /// * `limit_width` - text wrapping limit. Lines that are longer than this parameter will + // be split into smaller lines. + /// + /// # Returns + /// + /// An iterator of type `LinesWithLimit` that yields each line as a `&str`. + /// + /// # Examples + /// + /// ``` + /// let text = "Hello\nWorld\n"; + /// let mut lines = format_tools::string::lines_with_limit( text, 3 ); + /// assert_eq!( lines.next(), Some( "Hel" ) ); + /// assert_eq!( lines.next(), Some( "lo" ) ); + /// assert_eq!( lines.next(), Some( "Wor" ) ); + /// assert_eq!( lines.next(), Some( "ld" ) ); + /// assert_eq!( lines.next(), Some( "" ) ); + /// assert_eq!( lines.next(), None ); + /// ``` + pub fn lines_with_limit< S : AsRef< str > + ?Sized > + ( + src : & S, + limit_width : usize + ) + -> LinesWithLimit< '_ > + { + LinesWithLimit::new( src.as_ref(), limit_width ) + } + /// An iterator over the lines of a string slice. /// /// This struct implements the `Iterator` trait, allowing you to iterate over the lines @@ -128,6 +169,7 @@ mod private has_trailing_newline : bool, finished : bool, } + impl< 'a > Lines< 'a > { fn new( input : &'a str ) -> Self @@ -172,6 +214,70 @@ mod private } } + /// An iterator over the lines of a string slice with text wrapping. + /// + /// This struct implements the `Iterator` trait, allowing you to iterate over the parts + /// of a string. It uses `Lines` iterator and splits lines if they are longer that the + /// `limit_width` parameter. If the string contains a trailing new line, then an empty + /// string will be yielded in this iterator. + /// + /// If `limit_width` is equal to 0, then no wrapping is applied, and behaviour of this + /// iterator is equals to `Lines` iterator. + #[ derive( Debug ) ] + pub struct LinesWithLimit< 'a > + { + lines : Lines< 'a >, + limit_width : usize, + cur : Option< &'a str >, + } + + impl< 'a > LinesWithLimit< 'a > + { + fn new( input : &'a str, limit_width : usize ) -> Self + { + LinesWithLimit + { + lines : lines( input ), + limit_width, + cur : None, + } + } + } + + impl< 'a > Iterator for LinesWithLimit< 'a > + { + type Item = &'a str; + + fn next( &mut self ) -> Option< Self::Item > + { + if self.cur.is_none() || self.cur.is_some_and( str::is_empty ) + { + self.cur = self.lines.next(); + } + + match self.cur + { + None => return None, + + Some( cur ) => + { + if self.limit_width == 0 + { + self.cur = None; + Some( cur ) + } + else + { + let (chunk, rest) = cur.split_at(self.limit_width.min(cur.len())); + self.cur = Some( rest ); + + Some(chunk) + } + } + } + } + } + } #[ allow( unused_imports ) ] @@ -191,6 +297,8 @@ pub mod own size, lines, Lines, + lines_with_limit, + LinesWithLimit, }; } diff --git a/module/core/format_tools/src/format/text_wrap.rs b/module/core/format_tools/src/format/text_wrap.rs new file mode 100644 index 0000000000..695ac287cd --- /dev/null +++ b/module/core/format_tools/src/format/text_wrap.rs @@ -0,0 +1,256 @@ +//! +//! Text wrapping function. +//! + +/// Define a private namespace for all its items. +mod private +{ + + use std::borrow::Cow; + + use crate::*; + + /// Struct that represents a wrapped tabular data. It is similar to `InputExtract`, + /// but we cannot use it as it does not wrap the text and it contains wrong column + /// widthes and heights (as they are dependent on wrapping too). + #[ derive( Debug ) ] + pub struct WrappedInputExtract< 'data > + { + /// Tabular data of rows and columns. + /// Note: these cells does not represent the actual information cells in the + /// original table. These cells are wrapped and used only for displaying. This also + /// means that one row in original table can be represented here with one or more + /// rows. + pub data: Vec< Vec< WrappedCell< 'data > > >, + + /// New widthes of columns that include wrapping. + pub column_widthes : Vec< usize >, + + /// Size of the first row of the table. + /// This parameter is used in case header of the table should be displayed. + pub first_row_height : usize, + } + + /// Struct that represents a content of a wrapped cell. + /// It contains the slice of the cell as well as its original width. + /// + /// Parameter `wrap_width` is needed as text in `output_format::Table` is centered. + /// However it is centered according to whole cell size and not the size of wrapped + /// text slice. + /// + /// Example that depicts the importance of `wrap_width` parameter: + /// + /// 1) | [ | 2) | [ | + /// | line1, | | line1, | + /// | line2 | | line2 | + /// | ] | | ] | + /// + /// The first case seems to be properly formatted, while the second case took centering + /// too literally. That is why `wrap_width` is introduced, and additional spaces to the + /// right side should be included by the output formatter. + #[ derive( Debug ) ] + pub struct WrappedCell< 'data > + { + /// Width of the cell. In calculations use this width instead of slice length in order + /// to properly center the text. See example in the doc string of the parent struct. + pub wrap_width : usize, + + /// Actual content of the cell. + pub content : Cow< 'data, str > + } + + /// Wrap table cells. + /// + /// `InputExtract` contains cells with full content, so it represents the logical + /// structure of the table. `WrappedInputExtract` wraps original cells to smaller + /// cells. The resulting data is more low-level and corresponds to the table that + /// will be actually printed to the console (or other output type). + /// + /// `InputExtract` is not directly passed to this function, as it made to be general. + /// Instead you pass table cells in `data` argument and pass a vector of column widthes + /// in `columns_width_list` generated by `InputExtract`. + /// + /// `columns_width_list` is a slice, this is more effective and general than just a `Vec`. + /// In table style, there could be many columns, but in records style there will be + /// always 2 columns - this number is known at compile time, so we can use a slice object. + /// + /// Notice: + /// 1. Data passed to this function should contain only visible rows and columns. + /// It does not perform additional filtering. + /// 2. `data` parameters is **vector of rows of columns** (like and ordinary table). + /// This means that in styles like `Records` where headers and rows turned into columns + /// You have to transpose your data before passing it to this function. + /// + /// Wrapping is controlled by `columns_max_width` and `columns_nowrap_width` parameters. + /// + /// - `columns_max_width` is the size that is allowed to be occupied by columns. + /// It equals to maximum table width minus lengthes of visual elements (prefixes, + /// postfixes, separators, etc.). + /// + /// - `columns_nowrap_width` is the sum of column widthes of cells without wrapping (basically, + /// the sum of widthes of column descriptors in `InputExtract`). + /// + /// The function will perform wrapping and shrink the columns so that they occupy not + /// more than `columns_max_width`. + /// + /// If `columns_max_width` is equal to 0, then no wrapping will be performed. + pub fn text_wrap< 'data > + ( + data : impl Iterator< Item = &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > >, + columns_width_list : impl AsRef< [ usize ] >, + columns_max_width : usize, + columns_nowrap_width : usize, + ) + -> WrappedInputExtract< 'data > + { + let columns_width_list = columns_width_list.as_ref(); + + let mut first_row_height = 0; + let mut new_data = Vec::new(); + let mut column_widthes = Vec::new(); + + if columns_max_width == 0 || columns_max_width >= columns_nowrap_width + { + column_widthes.extend( columns_width_list.iter() ); + } + else + { + let shrink_factor : f32 = ( columns_max_width as f32 ) / ( columns_nowrap_width as f32 ); + + for ( icol, col_width ) in columns_width_list.iter().enumerate() + { + let col_limit_float = ( *col_width as f32 ) * shrink_factor; + let col_limit = col_limit_float.floor() as usize; + + let col_width_to_put = if icol == columns_width_list.len() - 1 + { + columns_max_width - column_widthes.iter().sum::() + } + else + { + col_limit.max(1) + }; + + column_widthes.push( col_width_to_put ); + } + } + + for ( irow, row ) in data.enumerate() + { + let mut wrapped_rows : Vec< Vec< Cow< 'data, str > > > = vec![]; + + for ( icol, col ) in row.iter().enumerate() + { + let col_limit = column_widthes[ icol ]; + let wrapped_col = string::lines_with_limit( col.0.as_ref(), col_limit ).map( Cow::from ).collect(); + wrapped_rows.push( wrapped_col ); + } + + let max_rows = wrapped_rows.iter().map( Vec::len ).max().unwrap_or(0); + + let mut transposed : Vec< Vec< WrappedCell< 'data > > > = Vec::new(); + + if max_rows == 0 + { + transposed.push( vec![] ); + } + + for i in 0..max_rows + { + let mut row_vec : Vec< WrappedCell< 'data > > = Vec::new(); + + for col_lines in &wrapped_rows + { + if col_lines.len() > i + { + let wrap_width = col_lines.iter().map( |c| c.len() ).max().unwrap_or(0); + row_vec.push( WrappedCell { wrap_width , content : col_lines[ i ].clone() } ); + } + else + { + row_vec.push( WrappedCell { wrap_width : 0, content : Cow::from( "" ) } ); + } + } + + transposed.push( row_vec ); + } + + if irow == 0 + { + first_row_height += transposed.len(); + } + + new_data.extend( transposed ); + } + + WrappedInputExtract + { + data: new_data, + first_row_height, + column_widthes + } + } + + /// Calculate width of the column without wrapping. + pub fn width_calculate< 'data > + ( + column : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > + ) + -> usize + { + column.iter().map( |k| + { + string::lines( k.0.as_ref() ).map( |l| l.chars().count() ).max().unwrap_or( 0 ) + } ).max().unwrap_or( 0 ) + } + +} + +#[ allow( unused_imports ) ] +pub use own::*; + +/// Own namespace of the module. +#[ allow( unused_imports ) ] +pub mod own +{ + use super::*; + #[ doc( inline ) ] + pub use orphan::*; + + #[ doc( inline ) ] + pub use + { + }; + + #[ doc( inline ) ] + pub use private:: + { + text_wrap, + width_calculate, + }; + +} + +/// Orphan namespace of the module. +#[ allow( unused_imports ) ] +pub mod orphan +{ + use super::*; + #[ doc( inline ) ] + pub use exposed::*; +} + +/// Exposed namespace of the module. +#[ allow( unused_imports ) ] +pub mod exposed +{ + use super::*; + pub use super::super::output_format; +} + +/// Prelude to use essentials: `use my_module::prelude::*`. +#[ allow( unused_imports ) ] +pub mod prelude +{ + use super::*; +} diff --git a/module/core/format_tools/tests/inc/format_records_test.rs b/module/core/format_tools/tests/inc/format_records_test.rs index 72f23a5ff5..77b8de7364 100644 --- a/module/core/format_tools/tests/inc/format_records_test.rs +++ b/module/core/format_tools/tests/inc/format_records_test.rs @@ -316,4 +316,136 @@ fn filter_row_callback() // -// xxx : enable \ No newline at end of file +// xxx : enable + +#[ test ] +fn test_width_limiting() +{ + use the_module::string; + + for width in min_width()..max_width() + { + println!("width: {}", width); + + let test_objects = test_object::test_objects_gen(); + let as_table = AsTable::new( &test_objects ); + + let mut format = output_format::Records::default(); + format.max_width = width; + + let mut output = String::new(); + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( &mut output, printer ); + + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + + assert!( got.is_ok() ); + + for line in string::lines( &output ) + { + if line.starts_with(" = ") + { + continue; + } + + if line.chars().count() > width + { + println!("{}", output); + } + + assert!( line.chars().count() <= width ); + } + } +} + +#[ test ] +fn test_error_on_unsatisfiable_limit() +{ + // 0 is a special value that signifies no limit. + for width in 1..( min_width() ) + { + println!( "width: {}", width ); + + let test_objects = test_object::test_objects_gen(); + let as_table = AsTable::new( &test_objects ); + + let mut format = output_format::Records::default(); + format.max_width = width; + + let mut output = String::new(); + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( &mut output, printer ); + + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + + assert!( got.is_err() ); + } +} + +#[ test ] +fn test_table_not_grows() +{ + use the_module::string; + + let expected_width = max_width(); + + // The upper bound was chosen arbitrarily. + for width in ( expected_width + 1 )..500 + { + println!( "width: {}", width ); + + let test_objects = test_object::test_objects_gen(); + let as_table = AsTable::new( &test_objects ); + + let mut format = output_format::Records::default(); + format.max_width = width; + + let mut output = String::new(); + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( &mut output, printer ); + + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + + assert!( got.is_ok() ); + println!("{}", output); + + for line in string::lines( &output ) + { + if line.starts_with(" = ") + { + continue; + } + + assert!( line.chars().count() <= expected_width ); + } + } +} + +/// Utility function for calculating minimum table width with `test_objects_gen()` with +/// the default table style. +fn min_width() -> usize +{ + let format = output_format::Records::default(); + format.min_width() +} + +/// Utility function for calculating default table width with `test_objects_gen()` with +/// the default table style with table width limit equals to 0. +fn max_width() -> usize +{ + use the_module::string; + + let test_objects = test_object::test_objects_gen(); + let as_table = AsTable::new( &test_objects ); + + let format = output_format::Records::default(); + + let mut output = String::new(); + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( &mut output, printer ); + + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + assert!( got.is_ok() ); + + string::lines( &output ).map( |s| s.chars().count() ).max().unwrap_or(0) +} \ No newline at end of file diff --git a/module/core/format_tools/tests/inc/format_table_test.rs b/module/core/format_tools/tests/inc/format_table_test.rs index 4f4d6694d2..945696f572 100644 --- a/module/core/format_tools/tests/inc/format_table_test.rs +++ b/module/core/format_tools/tests/inc/format_table_test.rs @@ -8,9 +8,6 @@ use the_module:: filter, print, output_format, - print::{ InputExtract, RowDescriptor, ColDescriptor }, - TableOutputFormat, - filter::LineType }; use std:: @@ -347,4 +344,130 @@ fn no_subtract_with_overflow() let result = the_module::TableFormatter::fmt( &as_table, &mut context ); assert!( result.is_ok() ); +} + +#[ test ] +fn test_width_limiting() +{ + use the_module::string; + + for max_width in min_width()..max_width() + { + println!("max_width: {}", max_width); + + let test_objects = test_object::test_objects_gen(); + let as_table = AsTable::new( &test_objects ); + + let mut format = output_format::Table::default(); + format.max_width = max_width; + + let mut output = String::new(); + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( &mut output, printer ); + + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + + assert!( got.is_ok() ); + + for line in string::lines( &output ) + { + assert_eq!( max_width, line.chars().count() ); + } + } +} + +#[ test ] +fn test_error_on_unsatisfiable_limit() +{ + // 0 is a special value that signifies no limit. Therefore, the lower bound is 1. + for max_width in 1..( min_width() ) + { + println!( "max_width: {}", max_width ); + + let test_objects = test_object::test_objects_gen(); + let as_table = AsTable::new( &test_objects ); + + let mut format = output_format::Table::default(); + format.max_width = max_width; + + let mut output = String::new(); + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( &mut output, printer ); + + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + + assert!( got.is_err() ); + } +} + +#[ test ] +fn test_table_not_grows() +{ + use the_module::string; + + let expected_width = max_width(); + + // The upper bound was chosen arbitrarily. + for max_width in ( expected_width + 1 )..500 + { + println!( "max_width: {}", max_width ); + + let test_objects = test_object::test_objects_gen(); + let as_table = AsTable::new( &test_objects ); + + let mut format = output_format::Table::default(); + format.max_width = max_width; + + let mut output = String::new(); + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( &mut output, printer ); + + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + + assert!( got.is_ok() ); + + for line in string::lines( &output ) + { + assert_eq!( expected_width, line.chars().count() ); + } + } +} + +/// Utility function for calculating minimum table width with `test_objects_gen()` with +/// the default table style. +fn min_width() -> usize +{ + use the_module::Fields; + + let format = output_format::Table::default(); + let test_objects = test_object::test_objects_gen(); + let col_count = test_objects[0].fields().count(); + + format.min_width( col_count ) +} + +/// Utility function for calculating default table width with `test_objects_gen()` with +/// the default table style without any maximum width. +fn max_width() -> usize +{ + use the_module::string; + + let test_objects = test_object::test_objects_gen(); + let as_table = AsTable::new( &test_objects ); + + let format = output_format::Table::default(); + + let mut output = String::new(); + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( &mut output, printer ); + + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + assert!( got.is_ok() ); + + for line in string::lines( &output ) + { + return line.chars().count(); + } + + 0 } \ No newline at end of file diff --git a/module/core/format_tools/tests/tests.rs b/module/core/format_tools/tests/tests.rs index 4fca6dbc07..c8e636300b 100644 --- a/module/core/format_tools/tests/tests.rs +++ b/module/core/format_tools/tests/tests.rs @@ -1,6 +1,6 @@ //! Primary tests. -#![ feature( trace_macros ) ] +// #![ feature( trace_macros ) ] #![ allow( unused_imports ) ] use format_tools as the_module;