From 022601e64f1141a5bce09a04594d101cf6b49c25 Mon Sep 17 00:00:00 2001 From: InAnYan Date: Fri, 15 Nov 2024 15:16:40 +0200 Subject: [PATCH 01/12] Remove slices --- module/core/format_tools/src/format/print.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/module/core/format_tools/src/format/print.rs b/module/core/format_tools/src/format/print.rs index 78ac91b294..88a9cff8df 100644 --- a/module/core/format_tools/src/format/print.rs +++ b/module/core/format_tools/src/format/print.rs @@ -282,11 +282,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 >, - } // From 2ff39baad86d5f212b4f538ff26b737cdbab2f4f Mon Sep 17 00:00:00 2001 From: InAnYan Date: Fri, 15 Nov 2024 15:24:42 +0200 Subject: [PATCH 02/12] Start moving --- .../src/format/output_format/records.rs | 2 +- .../src/format/output_format/table.rs | 187 ++++++++++-------- module/core/format_tools/src/format/print.rs | 68 +------ 3 files changed, 109 insertions(+), 148 deletions(-) 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..ada3eb8242 100644 --- a/module/core/format_tools/src/format/output_format/records.rs +++ b/module/core/format_tools/src/format/output_format/records.rs @@ -190,7 +190,7 @@ impl TableOutputFormat for Records for islice in 0..height { - let label = x.header_slice( islice, icol ); + let label = x.header_slice( icol ); let md_index = [ islice, icol, irow ]; let slice = x.slices[ x.slices_dim.md_offset( md_index ) ]; diff --git a/module/core/format_tools/src/format/output_format/table.rs b/module/core/format_tools/src/format/output_format/table.rs index e96af36f20..9b7281ee97 100644 --- a/module/core/format_tools/src/format/output_format/table.rs +++ b/module/core/format_tools/src/format/output_format/table.rs @@ -16,6 +16,7 @@ use print:: InputExtract, Context, }; +use std::borrow::Cow; use core:: { fmt, @@ -163,8 +164,6 @@ impl TableOutputFormat for Table { fn extract_write< 'buf, 'data >( &self, x : &InputExtract< 'data >, c : &mut Context< 'buf > ) -> fmt::Result { - use md_math::MdOffset; - let cell_prefix = &self.cell_prefix; let cell_postfix = &self.cell_postfix; let cell_separator = &self.cell_separator; @@ -173,114 +172,134 @@ 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 data = wrap_text( &x.data, self.max_width ); + + let column_count = x.header().count(); + + let mut col_width : Vec< usize > = vec![ 0; column_count ]; + + for row in data.iter() { - 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 + for ( icol, col ) in row.iter().enumerate() { - grid_width += ( x.mcells_vis[ 0 ] - 1 ) * ( cell_separator.chars().count() ); + col_width[ icol ] = col_width[ icol ].max( col.content.chars().count() ); } - x.mchars[ 0 ] + grid_width } - else - { - 0 - }; - let mut prev_typ : Option< LineType > = None; - // dbg!( x.row_descriptors.len() ); + let max_row_width = col_width.iter().sum::() + + 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() }; - for ( irow, row ) in x.row_descriptors.iter().enumerate() - { - let height = row.height; + let mut actual_rows = 0; - if delimitting_header + for row in data.iter() + { + if actual_rows == 1 && x.has_header && self.delimitting_header { - 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 ); - } + write!( c.buf, "{}", row_separator )?; + write!( c.buf, "{}", h.repeat( max_row_width ) )?; } - - if !row.vis + + if actual_rows > 0 { - continue; + write!( c.buf, "{}", row_separator )?; } - // dbg!( row.height ); + actual_rows += 1; + + write!( c.buf, "{}", row_prefix )?; - for islice in 0..height + for ( icol, col ) in row.iter().enumerate() { + let cell_width = col.wrap_width; + let col_width = col_width[ icol ]; + let slice_width = col.content.chars().count(); - if irow > 0 + 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 = ( col_width - cell_width ) / 2; + let rspaces = ( ( col_width - cell_width ) as f32 / 2 as f32 ).round() as usize + cell_width - slice_width; + + if lspaces > 0 + { + write!( c.buf, "{: 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 = if cell_width > width { - 0 - } else { - ( width - cell_width ) / 2 - }; - - let rspaces = if (cell_width > width) || (slice.len() > cell_width) { - 0 - } else { - ( width - cell_width + 1 ) / 2 + cell_width - slice.len() - }; - - // 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, "{}", cell_postfix )?; + write!( c.buf, "{:>width$}", " ", width = rspaces )?; } - write!( c.buf, "{}", row_postfix )?; + write!( c.buf, "{}", cell_postfix )?; } + write!( c.buf, "{}", row_postfix )?; } Ok(()) } } + +#[ derive( Debug ) ] +struct WrappedCell< 'data > +{ + wrap_width : usize, + content : Cow< 'data, str > +} + +fn wrap_text< 'data > +( + data: &'data Vec< Vec< Cow< 'data, str > > >, + limit: usize +) +-> Vec< Vec< WrappedCell< 'data > > > +{ + let mut new_data = Vec::new(); + + for ( id, row ) in data + { + let unwrapped_text : Vec< Vec< Cow< 'data, str > > > = row.iter().map( |c| string::lines_with_limit( c.as_ref(), limit ).map( Cow::from ).collect() ).collect(); + + let max_rows = unwrapped_text.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 &unwrapped_text + { + 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 ); + } + + new_data.extend(transposed); + } + + new_data +} \ 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 88a9cff8df..fbf7cfd51d 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:: @@ -335,24 +334,19 @@ 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 { @@ -378,8 +372,6 @@ mod private 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 ]; @@ -527,23 +519,7 @@ 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::< '_ > + let x = InputExtract::< '_ > { mcells, mcells_vis, @@ -552,42 +528,8 @@ 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() - { - - 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(); - } - } - - } - - std::mem::swap( &mut x.slices, &mut slices ); - return callback( &x ); } From 624350567c6de13777162d1a6e5d611f5fec6fe2 Mon Sep 17 00:00:00 2001 From: InAnYan Date: Fri, 15 Nov 2024 16:17:50 +0200 Subject: [PATCH 03/12] Tests are passing --- .../src/format/output_format/records.rs | 77 +++++++++------ .../src/format/output_format/table.rs | 23 ++++- module/core/format_tools/src/format/print.rs | 11 ++- module/core/format_tools/src/format/string.rs | 98 +++++++++++++++++++ .../tests/inc/to_string_with_fallback_test.rs | 2 +- 5 files changed, 174 insertions(+), 37 deletions(-) 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 ada3eb8242..2d2a8e46c5 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. @@ -108,6 +110,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 +135,7 @@ impl Default for Records cell_prefix, cell_postfix, cell_separator, + max_width, // h, // v, // t_l, @@ -155,70 +160,82 @@ impl TableOutputFormat for Records c : & mut Context< 'buf >, ) -> fmt::Result { - - let label_width = x.header().fold( 0, | acc, cell | acc.max( cell.1[ 0 ] ) ); + let field_names : Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > = x.header().collect(); + let key_width = x.header().fold( 0, | acc, cell | acc.max( cell.0.chars().count() ) ); write!( c.buf, "{}", self.table_prefix )?; - let mut first = true; - // Write each record - for ( irow, row ) in x.rows() - { + let mut actual_entries = 0; - if !row.vis + for ( ientry_descriptor, entry_descriptor ) in x.row_descriptors.iter().enumerate() + { + if !entry_descriptor.vis || ( x.has_header && ientry_descriptor == 0 ) { continue; } - if first - { - first = false; - } - else + if actual_entries > 0 { write!( c.buf, "{}", self.table_separator )?; } - let slice_width = x.data[ irow ].iter().fold( 0, | acc, cell | acc.max( cell.1[ 0 ] ) ); + actual_entries += 1; - writeln!( c.buf, " = {}", irow )?; + writeln!( c.buf, " = {}", entry_descriptor.irow )?; - for ( icol, _col ) in x.col_descriptors.iter().enumerate() - { - let cell = &x.data[ irow ][ icol ]; - let height = cell.1[ 1 ]; + let row = wrap_text( &x.data[ ientry_descriptor ], 0 ); - for islice in 0..height - { - let label = x.header_slice( icol ); - let md_index = [ islice, icol, irow ]; - let slice = x.slices[ x.slices_dim.md_offset( md_index ) ]; + let value_width = row.iter().map( |sr| sr.iter().map( |c| c.chars().count() ).max().unwrap_or(0) ).max().unwrap_or(0); - if icol > 0 || islice > 0 + let mut row_count = 0; + + for ( ifield, field ) in row.iter().enumerate() + { + for ( irow, row ) in field.iter().enumerate() + { + if row_count > 0 { write!( c.buf, "{}", self.row_separator )?; } + row_count += 1; + let key = if irow > 0 + { + "" + } + else + { + field_names.get( ifield ).map( |c| c.0.as_ref() ).unwrap_or( "" ) + }; + write!( c.buf, "{}", self.row_prefix )?; write!( c.buf, "{}", self.cell_prefix )?; - write!( c.buf, "{: +( + data: &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) >, + limit: usize +) +-> Vec< Vec< &'data str > > +{ + data.iter().map( |c| string::lines_with_limit( c.0.as_ref(), limit ).collect() ).collect() +} diff --git a/module/core/format_tools/src/format/output_format/table.rs b/module/core/format_tools/src/format/output_format/table.rs index 9b7281ee97..28f0df8641 100644 --- a/module/core/format_tools/src/format/output_format/table.rs +++ b/module/core/format_tools/src/format/output_format/table.rs @@ -14,6 +14,7 @@ use crate::*; use print:: { InputExtract, + RowDescriptor, Context, }; use std::borrow::Cow; @@ -76,6 +77,8 @@ pub struct Table 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 @@ -103,6 +106,7 @@ impl Default for Table let corner_rt = '┐'; let corner_lb = '└'; let corner_rb = '┘'; + let max_width = 0; Self { @@ -124,6 +128,7 @@ impl Default for Table corner_rt, corner_lb, corner_rb, + max_width } } } @@ -172,7 +177,7 @@ impl TableOutputFormat for Table let row_separator = &self.row_separator; let h = self.h.to_string(); - let data = wrap_text( &x.data, self.max_width ); + let data = wrap_text( &x.data, &x.row_descriptors, self.max_width ); let column_count = x.header().count(); @@ -258,16 +263,24 @@ struct WrappedCell< 'data > fn wrap_text< 'data > ( - data: &'data Vec< Vec< Cow< 'data, str > > >, - limit: usize + data : &'data Vec< Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > >, + row_descriptors : &Vec< RowDescriptor >, + limit : usize ) -> Vec< Vec< WrappedCell< 'data > > > { let mut new_data = Vec::new(); - for ( id, row ) in data + for ( irow, row ) in data.iter().enumerate() { - let unwrapped_text : Vec< Vec< Cow< 'data, str > > > = row.iter().map( |c| string::lines_with_limit( c.as_ref(), limit ).map( Cow::from ).collect() ).collect(); + let row_descriptor = &row_descriptors[ irow ]; + + if !row_descriptor.vis + { + continue; + } + + let unwrapped_text : Vec< Vec< Cow< 'data, str > > > = row.iter().map( |c| string::lines_with_limit( c.0.as_ref(), limit ).map( Cow::from ).collect() ).collect(); let max_rows = unwrapped_text.iter().map( Vec::len ).max().unwrap_or(0); diff --git a/module/core/format_tools/src/format/print.rs b/module/core/format_tools/src/format/print.rs index fbf7cfd51d..a2656d528e 100644 --- a/module/core/format_tools/src/format/print.rs +++ b/module/core/format_tools/src/format/print.rs @@ -519,7 +519,7 @@ 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 } ); - let x = InputExtract::< '_ > + let mut x = InputExtract::< '_ > { mcells, mcells_vis, @@ -530,6 +530,14 @@ mod private has_header, }; + if x.data.len() > 0 + { + for icol in 0 .. x.col_descriptors.len() + { + x.col_descriptors[ icol ].label = x.data[ 0 ][ icol ].0.as_ref(); + } + } + return callback( &x ); } @@ -554,6 +562,7 @@ pub mod own Context, Printer, InputExtract, + RowDescriptor, }; } diff --git a/module/core/format_tools/src/format/string.rs b/module/core/format_tools/src/format/string.rs index 619d1690c2..1cf3986af0 100644 --- a/module/core/format_tools/src/format/string.rs +++ b/module/core/format_tools/src/format/string.rs @@ -84,6 +84,35 @@ mod private [ width, height ] } + pub fn size_with_limit< S : AsRef< str > > + ( + src : S, + limit_width : usize, + ) + -> [ usize; 2 ] + { + if limit_width == 0 + { + return size( src ); + } + + let text = src.as_ref(); + let mut height = 0; + let mut width = 0; + + for line in lines_with_limit( text, limit_width ) + { + height += 1; + let line_length = line.len(); + if line_length > width + { + width = line_length; + } + } + + [ width, height ] + } + /// Returns an iterator over the lines of a string slice. /// /// This function provides an iterator that yields each line of the input string slice. @@ -114,6 +143,16 @@ mod private Lines::new( src.as_ref() ) } + 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 +167,7 @@ mod private has_trailing_newline : bool, finished : bool, } + impl< 'a > Lines< 'a > { fn new( input : &'a str ) -> Self @@ -172,6 +212,61 @@ mod private } } + #[ 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 ) ] @@ -189,8 +284,11 @@ pub mod own pub use private:: { size, + size_with_limit, lines, Lines, + lines_with_limit, + LinesWithLimit, }; } diff --git a/module/core/format_tools/tests/inc/to_string_with_fallback_test.rs b/module/core/format_tools/tests/inc/to_string_with_fallback_test.rs index bd9947cd71..9491237b6d 100644 --- a/module/core/format_tools/tests/inc/to_string_with_fallback_test.rs +++ b/module/core/format_tools/tests/inc/to_string_with_fallback_test.rs @@ -14,7 +14,7 @@ use the_module:: use std:: { - // fmt, + fmt, // collections::HashMap, borrow::Cow, }; From 98c5bce271fce25aac07fd26ae7f895b4a4eed54 Mon Sep 17 00:00:00 2001 From: InAnYan Date: Fri, 15 Nov 2024 17:19:12 +0200 Subject: [PATCH 04/12] Not ideal --- .../src/format/output_format/table.rs | 89 ++++++++++++------- module/core/format_tools/src/format/print.rs | 1 - .../tests/inc/format_table_test.rs | 38 ++++++++ 3 files changed, 96 insertions(+), 32 deletions(-) diff --git a/module/core/format_tools/src/format/output_format/table.rs b/module/core/format_tools/src/format/output_format/table.rs index 28f0df8641..cb2b1a8679 100644 --- a/module/core/format_tools/src/format/output_format/table.rs +++ b/module/core/format_tools/src/format/output_format/table.rs @@ -14,7 +14,6 @@ use crate::*; use print:: { InputExtract, - RowDescriptor, Context, }; use std::borrow::Cow; @@ -177,34 +176,27 @@ impl TableOutputFormat for Table let row_separator = &self.row_separator; let h = self.h.to_string(); - let data = wrap_text( &x.data, &x.row_descriptors, self.max_width ); + let column_count = x.col_descriptors.len(); - let column_count = x.header().count(); - - let mut col_width : Vec< usize > = vec![ 0; column_count ]; - - for row in data.iter() - { - for ( icol, col ) in row.iter().enumerate() - { - col_width[ icol ] = col_width[ icol ].max( col.content.chars().count() ); - } - } - - let max_row_width = col_width.iter().sum::() - + self.row_prefix.chars().count() + let unchangable_width = 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() }; + let original_row_width = x.col_descriptors.iter().map( |c| c.width ).sum::() + unchangable_width; + + let wrapped_text = wrap_text( &x, self.max_width, original_row_width ); + + let new_row_width = wrapped_text.col_widthes.iter().sum::() + unchangable_width; + let mut actual_rows = 0; - for row in data.iter() + for row in wrapped_text.data.iter() { - if actual_rows == 1 && x.has_header && self.delimitting_header + if actual_rows == wrapped_text.first_row_height && x.has_header && self.delimitting_header { write!( c.buf, "{}", row_separator )?; - write!( c.buf, "{}", h.repeat( max_row_width ) )?; + write!( c.buf, "{}", h.repeat( new_row_width ) )?; } if actual_rows > 0 @@ -219,9 +211,9 @@ impl TableOutputFormat for Table for ( icol, col ) in row.iter().enumerate() { let cell_width = col.wrap_width; - let col_width = col_width[ icol ]; + let col_width = wrapped_text.col_widthes[ icol ]; let slice_width = col.content.chars().count(); - + if icol > 0 { write!( c.buf, "{}", cell_separator )?; @@ -254,6 +246,14 @@ impl TableOutputFormat for Table } } +#[ derive( Debug ) ] +struct WrappedInputExtract< 'data > +{ + data: Vec< Vec< WrappedCell< 'data > > >, + col_widthes : Vec< usize >, + first_row_height : usize, +} + #[ derive( Debug ) ] struct WrappedCell< 'data > { @@ -263,26 +263,43 @@ struct WrappedCell< 'data > fn wrap_text< 'data > ( - data : &'data Vec< Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > >, - row_descriptors : &Vec< RowDescriptor >, - limit : usize + x : &'data InputExtract< 'data >, + limit : usize, + orig_table_width : usize, ) --> Vec< Vec< WrappedCell< 'data > > > +-> WrappedInputExtract< 'data > { + let mut first_row_height = 0; let mut new_data = Vec::new(); + + let mut col_widthes = Vec::new(); - for ( irow, row ) in data.iter().enumerate() + for col in &x.col_descriptors { - let row_descriptor = &row_descriptors[ irow ]; + let col_width = col.width; + let col_limit = ( limit as f32 * ( col_width as f32 / orig_table_width as f32 ) ) as usize; + col_widthes.push( if limit == 0 { col_width } else { col_limit.max(1) } ); + } + + for ( irow, row ) in x.data.iter().enumerate() + { + let row_descriptor = &x.row_descriptors[ irow ]; if !row_descriptor.vis { continue; } - let unwrapped_text : Vec< Vec< Cow< 'data, str > > > = row.iter().map( |c| string::lines_with_limit( c.0.as_ref(), limit ).map( Cow::from ).collect() ).collect(); + let mut wrapped_rows : Vec< Vec< Cow< 'data, str > > > = vec![]; + + for ( icol, col ) in row.iter().enumerate() + { + let col_limit = col_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 = unwrapped_text.iter().map( Vec::len ).max().unwrap_or(0); + let max_rows = wrapped_rows.iter().map( Vec::len ).max().unwrap_or(0); let mut transposed : Vec< Vec< WrappedCell< 'data > > > = Vec::new(); @@ -295,7 +312,7 @@ fn wrap_text< 'data > { let mut row_vec : Vec< WrappedCell< 'data > > = Vec::new(); - for col_lines in &unwrapped_text + for col_lines in &wrapped_rows { if col_lines.len() > i { @@ -311,8 +328,18 @@ fn wrap_text< 'data > transposed.push( row_vec ); } + if irow == 0 + { + first_row_height += transposed.len(); + } + new_data.extend(transposed); } - new_data + WrappedInputExtract + { + data: new_data, + first_row_height, + col_widthes + } } \ 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 a2656d528e..6c9c7bef21 100644 --- a/module/core/format_tools/src/format/print.rs +++ b/module/core/format_tools/src/format/print.rs @@ -562,7 +562,6 @@ pub mod own Context, Printer, InputExtract, - RowDescriptor, }; } 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 eb8a3b17dd..8eb6f54c33 100644 --- a/module/core/format_tools/tests/inc/format_table_test.rs +++ b/module/core/format_tools/tests/inc/format_table_test.rs @@ -326,3 +326,41 @@ fn filter_row_callback() // // xxx : implement test for vector of vectors + +#[ test ] +fn test_width_limiting() +{ + 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 = 50; + + let mut output = String::new(); + let mut 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 ); + + let exp = r#"│ id │ created_at │ file_ids │ tools │ +───────────────────────────────────────────────────────────────────── +│ 1 │ 1627845583 │ [ │ │ +│ │ │ "file1", │ │ +│ │ │ "file2", │ │ +│ │ │ ] │ │ +│ 2 │ 13 │ [ │ [ │ +│ │ │ "file3", │ { │ +│ │ │ "file4\nmore detai │ "tool1": "valu │ +│ │ │ ls", │ e1", │ +│ │ │ ] │ }, │ +│ │ │ │ { │ +│ │ │ │ "tool2": "valu │ +│ │ │ │ e2", │ +│ │ │ │ }, │ +│ │ │ │ ] │"#; + + a_id!( output.as_str(), exp ); +} \ No newline at end of file From 17422e075d079ee6380cf63967db4933986654ea Mon Sep 17 00:00:00 2001 From: InAnYan Date: Fri, 15 Nov 2024 17:19:43 +0200 Subject: [PATCH 05/12] Fix test --- module/core/format_tools/tests/inc/format_table_test.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 8eb6f54c33..a7d3724d94 100644 --- a/module/core/format_tools/tests/inc/format_table_test.rs +++ b/module/core/format_tools/tests/inc/format_table_test.rs @@ -334,7 +334,7 @@ fn test_width_limiting() let as_table = AsTable::new( &test_objects ); let mut format = output_format::Table::default(); - format.max_width = 50; + format.max_width = 69; let mut output = String::new(); let mut printer = print::Printer::with_format( &format ); @@ -345,6 +345,7 @@ fn test_width_limiting() assert!( got.is_ok() ); println!( "{}", &output ); + // This might not be the right output. let exp = r#"│ id │ created_at │ file_ids │ tools │ ───────────────────────────────────────────────────────────────────── │ 1 │ 1627845583 │ [ │ │ From 0b46e5d02d9ec3093139f0fc9696263b288a3dbf Mon Sep 17 00:00:00 2001 From: InAnYan Date: Mon, 18 Nov 2024 12:23:27 +0200 Subject: [PATCH 06/12] Fix algorithm and add tests --- .../src/format/output_format/table.rs | 63 +++++++-- .../tests/inc/format_table_test.rs | 123 +++++++++++++++--- module/core/format_tools/tests/tests.rs | 2 +- 3 files changed, 156 insertions(+), 32 deletions(-) diff --git a/module/core/format_tools/src/format/output_format/table.rs b/module/core/format_tools/src/format/output_format/table.rs index cb2b1a8679..1325253980 100644 --- a/module/core/format_tools/src/format/output_format/table.rs +++ b/module/core/format_tools/src/format/output_format/table.rs @@ -162,6 +162,20 @@ impl Table }) } + + /// Calculate minimum width of the table with specified numbers of columns. + pub fn calculate_minimum_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 @@ -183,11 +197,20 @@ impl TableOutputFormat for Table + 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() }; - let original_row_width = x.col_descriptors.iter().map( |c| c.width ).sum::() + unchangable_width; + let minimum_acceptable_width = column_count + unchangable_width; - let wrapped_text = wrap_text( &x, self.max_width, original_row_width ); + if self.max_width != 0 && ( unchangable_width + column_count > self.max_width ) + { + return Err( fmt::Error ); + } + + let orig_column_space = x.col_descriptors.iter().map( |c| c.width ).sum::(); + + let wrapped_text = wrap_text( &x, if self.max_width == 0 { 0 } else { self.max_width - unchangable_width }, orig_column_space ); - let new_row_width = wrapped_text.col_widthes.iter().sum::() + unchangable_width; + let new_column_space = wrapped_text.col_widthes.iter().sum::(); + + let new_row_width = new_column_space + unchangable_width; let mut actual_rows = 0; @@ -264,21 +287,41 @@ struct WrappedCell< 'data > fn wrap_text< 'data > ( x : &'data InputExtract< 'data >, - limit : usize, - orig_table_width : usize, + limit_column_space : usize, + orig_column_space : usize, ) -> WrappedInputExtract< 'data > { let mut first_row_height = 0; let mut new_data = Vec::new(); - let mut col_widthes = Vec::new(); - for col in &x.col_descriptors + if limit_column_space == 0 || limit_column_space >= orig_column_space { - let col_width = col.width; - let col_limit = ( limit as f32 * ( col_width as f32 / orig_table_width as f32 ) ) as usize; - col_widthes.push( if limit == 0 { col_width } else { col_limit.max(1) } ); + col_widthes.extend( x.col_descriptors.iter().map( |d| d.width ) ); + } + else + { + let shrink_factor: f32 = ( limit_column_space as f32 ) / ( orig_column_space as f32 ); + + for ( icol, col ) in x.col_descriptors.iter().enumerate() + { + let col_width = col.width; + + 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 == x.col_descriptors.len() - 1 + { + limit_column_space - col_widthes.iter().sum::() + } + else + { + col_limit.max(1) + }; + + col_widthes.push( col_width_to_put ); + } } for ( irow, row ) in x.data.iter().enumerate() 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 a7d3724d94..a33b1d3427 100644 --- a/module/core/format_tools/tests/inc/format_table_test.rs +++ b/module/core/format_tools/tests/inc/format_table_test.rs @@ -5,9 +5,11 @@ use the_module:: { AsTable, WithRef, + Fields, filter, print, output_format, + string }; use std:: @@ -329,39 +331,118 @@ fn filter_row_callback() #[ test ] fn test_width_limiting() +{ + for width in calculate_minimum_width()..calculate_maximum_width() + { + println!("width: {}", 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 = width; + + let mut output = String::new(); + let mut 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!( width, line.chars().count() ); + } + } +} + +#[ test ] +fn test_error_on_unsatisfiable_limit() +{ + // 0 is a special value that signifies no limit. + for width in 1..( calculate_minimum_width() ) + { + println!( "width: {}", 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 = width; + + let mut output = String::new(); + let mut 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() +{ + let expected_width = calculate_maximum_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::Table::default(); + format.max_width = width; + + let mut output = String::new(); + let mut 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 calculate_minimum_width() -> usize +{ + let format = output_format::Table::default(); + let test_objects = test_object::test_objects_gen(); + let col_count = test_objects[0].fields().count(); + + format.calculate_minimum_width( col_count ) +} + +/// Utility function for calculating default table width with `test_objects_gen()` with +/// the default table style with table width limit equals to 0. +fn calculate_maximum_width() -> usize { 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 = 69; let mut output = String::new(); let mut 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 ); - // This might not be the right output. - let exp = r#"│ id │ created_at │ file_ids │ tools │ -───────────────────────────────────────────────────────────────────── -│ 1 │ 1627845583 │ [ │ │ -│ │ │ "file1", │ │ -│ │ │ "file2", │ │ -│ │ │ ] │ │ -│ 2 │ 13 │ [ │ [ │ -│ │ │ "file3", │ { │ -│ │ │ "file4\nmore detai │ "tool1": "valu │ -│ │ │ ls", │ e1", │ -│ │ │ ] │ }, │ -│ │ │ │ { │ -│ │ │ │ "tool2": "valu │ -│ │ │ │ e2", │ -│ │ │ │ }, │ -│ │ │ │ ] │"#; + for line in string::lines( &output ) + { + return line.chars().count(); + } - a_id!( output.as_str(), exp ); + 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; From 80a9010741123c2d2ec3d0d906fa7073d9e6424c Mon Sep 17 00:00:00 2001 From: InAnYan Date: Mon, 18 Nov 2024 15:53:42 +0200 Subject: [PATCH 07/12] Fix for Records --- .../src/format/output_format/records.rs | 139 +++++++++++++----- .../tests/inc/format_records_test.rs | 130 +++++++++++++++- .../tests/inc/format_table_test.rs | 10 +- 3 files changed, 235 insertions(+), 44 deletions(-) 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 2d2a8e46c5..c1fef759cb 100644 --- a/module/core/format_tools/src/format/output_format/records.rs +++ b/module/core/format_tools/src/format/output_format/records.rs @@ -93,6 +93,20 @@ impl Records static INSTANCE : OnceLock< Records > = OnceLock::new(); INSTANCE.get_or_init( || Records::default() ) } + + /// Calculate minimum width of the output. + pub fn calculate_minimum_width + ( + &self, + ) -> usize + { + 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 + // 2 because there are only 2 columns: key and value. + } } impl Default for Records @@ -160,8 +174,15 @@ impl TableOutputFormat for Records c : & mut Context< 'buf >, ) -> fmt::Result { + if self.max_width != 0 && self.max_width < self.calculate_minimum_width() + { + return Err( fmt::Error ); + } + + // 2 because there are only 2 columns: key and value. + let allowed_cell_space = if self.max_width == 0 { 0 } else { self.max_width - self.calculate_minimum_width() + 2 }; + let field_names : Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > = x.header().collect(); - let key_width = x.header().fold( 0, | acc, cell | acc.max( cell.0.chars().count() ) ); write!( c.buf, "{}", self.table_prefix )?; @@ -183,43 +204,29 @@ impl TableOutputFormat for Records writeln!( c.buf, " = {}", entry_descriptor.irow )?; - let row = wrap_text( &x.data[ ientry_descriptor ], 0 ); - - let value_width = row.iter().map( |sr| sr.iter().map( |c| c.chars().count() ).max().unwrap_or(0) ).max().unwrap_or(0); - - let mut row_count = 0; + let mut wrapped_text = wrap_text( &field_names, &x.data[ ientry_descriptor ], allowed_cell_space ); - for ( ifield, field ) in row.iter().enumerate() + for ( irow, ( key, value ) ) in wrapped_text.data.iter().enumerate() { - for ( irow, row ) in field.iter().enumerate() + if irow != 0 { - if row_count > 0 - { - write!( c.buf, "{}", self.row_separator )?; - } - row_count += 1; - - let key = if irow > 0 - { - "" - } - else - { - field_names.get( ifield ).map( |c| c.0.as_ref() ).unwrap_or( "" ) - }; - - write!( c.buf, "{}", self.row_prefix )?; - - write!( c.buf, "{}", self.cell_prefix )?; - write!( c.buf, "{: +{ + data : Vec< ( &'data str, &'data str ) >, + key_width : usize, + value_width : usize, +} + fn wrap_text<'data> ( - data: &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) >, - limit: usize + keys : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) >, + values : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) >, + allowed_cell_space : usize, ) --> Vec< Vec< &'data str > > +-> WrappedInputExtract< 'data > { - data.iter().map( |c| string::lines_with_limit( c.0.as_ref(), limit ).collect() ).collect() + let mut data = Vec::new(); + let mut key_width = calculate_width( keys ); + let mut value_width = calculate_width( values ); + + let orig_cell_space = key_width + value_width; + + if allowed_cell_space != 0 && orig_cell_space > allowed_cell_space + { + let factor = ( allowed_cell_space as f32 ) / ( orig_cell_space as f32 ); + key_width = ( ( key_width as f32 ) * factor ).round() as usize; + value_width = allowed_cell_space - key_width; + } + + for i in 0..values.len() + { + let key = &keys[ i ]; + let value = &values[ i ]; + + let key_wrapped : Vec< &'data str > = string::lines_with_limit( key.0.as_ref(), key_width ).collect(); + let value_wrapped : Vec< &'data str > = string::lines_with_limit( value.0.as_ref(), value_width ).collect(); + + for j in 0..( key_wrapped.len().max( value_wrapped.len() ) ) + { + let key = key_wrapped.get( j ).copied().unwrap_or( "" ); + let value = value_wrapped.get( j ).copied().unwrap_or( "" ); + + data.push( ( key, value ) ); + } + } + + WrappedInputExtract + { + data, + key_width, + value_width, + } } + +fn calculate_width< 'data > +( + vec : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > +) +-> usize +{ + vec.iter().map( |k| + { + string::lines( k.0.as_ref() ).map( |l| l.chars().count() ).max().unwrap_or( 0 ) + } ).max().unwrap_or( 0 ) +} \ No newline at end of file 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..62387eb1f1 100644 --- a/module/core/format_tools/tests/inc/format_records_test.rs +++ b/module/core/format_tools/tests/inc/format_records_test.rs @@ -5,9 +5,11 @@ use the_module:: { AsTable, WithRef, + Fields, filter, print, output_format, + string, }; use std:: @@ -316,4 +318,130 @@ fn filter_row_callback() // -// xxx : enable \ No newline at end of file +// xxx : enable + +#[ test ] +fn test_width_limiting() +{ + for width in calculate_minimum_width()..calculate_maximum_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 mut 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..( calculate_minimum_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() +{ + let expected_width = calculate_maximum_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 calculate_minimum_width() -> usize +{ + let format = output_format::Records::default(); + format.calculate_minimum_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 calculate_maximum_width() -> usize +{ + 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 a33b1d3427..3d68d0250a 100644 --- a/module/core/format_tools/tests/inc/format_table_test.rs +++ b/module/core/format_tools/tests/inc/format_table_test.rs @@ -343,7 +343,7 @@ fn test_width_limiting() format.max_width = width; let mut output = String::new(); - let mut printer = print::Printer::with_format( &format ); + 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 ); @@ -372,7 +372,7 @@ fn test_error_on_unsatisfiable_limit() format.max_width = width; let mut output = String::new(); - let mut printer = print::Printer::with_format( &format ); + 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 ); @@ -398,7 +398,7 @@ fn test_table_not_grows() format.max_width = width; let mut output = String::new(); - let mut printer = print::Printer::with_format( &format ); + 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 ); @@ -430,10 +430,10 @@ fn calculate_maximum_width() -> usize let test_objects = test_object::test_objects_gen(); let as_table = AsTable::new( &test_objects ); - let mut format = output_format::Table::default(); + let format = output_format::Table::default(); let mut output = String::new(); - let mut printer = print::Printer::with_format( &format ); + 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 ); From c1fda75053de5e2a2b0c4855a22ddca216ae4248 Mon Sep 17 00:00:00 2001 From: InAnYan Date: Tue, 19 Nov 2024 10:09:15 +0200 Subject: [PATCH 08/12] Fix from the feedback --- .../src/format/output_format/records.rs | 61 +++++++++++++--- .../src/format/output_format/table.rs | 66 +++++++++++++++-- module/core/format_tools/src/format/print.rs | 5 +- module/core/format_tools/src/format/string.rs | 70 +++++++++++-------- .../tests/inc/format_records_test.rs | 22 +++--- .../tests/inc/format_table_test.rs | 24 ++++--- 6 files changed, 184 insertions(+), 64 deletions(-) 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 c1fef759cb..de09187e8c 100644 --- a/module/core/format_tools/src/format/output_format/records.rs +++ b/module/core/format_tools/src/format/output_format/records.rs @@ -94,8 +94,23 @@ impl Records INSTANCE.get_or_init( || Records::default() ) } - /// Calculate minimum width of the output. - pub fn calculate_minimum_width + /// Calculate how much space is 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`. + /// + /// Is is the sum of: + /// - Length of `row_prefix`. + /// - Length of `row_postfix`. + /// - Length of `cell_prefix` and `cell_postfix` multiplied by 2. + /// - Length of `cell_separator` + /// - Just 2. + /// + /// 2 here is used as a constant because `output_format::Records` will generate tables only with + /// two columns (key and value). + /// + /// 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::Table` + /// style parameters. + pub fn min_width ( &self, ) -> usize @@ -105,7 +120,6 @@ impl Records + 2 * ( self.cell_postfix.chars().count() + self.cell_prefix.chars().count() ) + self.cell_separator.chars().count() + 2 - // 2 because there are only 2 columns: key and value. } } @@ -174,13 +188,13 @@ impl TableOutputFormat for Records c : & mut Context< 'buf >, ) -> fmt::Result { - if self.max_width != 0 && self.max_width < self.calculate_minimum_width() + 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 allowed_cell_space = if self.max_width == 0 { 0 } else { self.max_width - self.calculate_minimum_width() + 2 }; + let allowed_cell_space = if self.max_width == 0 { 0 } else { self.max_width - self.min_width() + 2 }; let field_names : Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > = x.header().collect(); @@ -204,7 +218,7 @@ impl TableOutputFormat for Records writeln!( c.buf, " = {}", entry_descriptor.irow )?; - let mut wrapped_text = wrap_text( &field_names, &x.data[ ientry_descriptor ], allowed_cell_space ); + let wrapped_text = text_wrap( &field_names, &x.data[ ientry_descriptor ], allowed_cell_space ); for ( irow, ( key, value ) ) in wrapped_text.data.iter().enumerate() { @@ -237,15 +251,41 @@ impl TableOutputFormat for Records } +/// 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 height (as they are dependent on wrapping, too). +/// +/// This struct is similar to `output_format::Table::WrappedInputExtract` (which is +/// private, too), but it is made only for 2 columns, as tables in `Records` contain +/// only key and value columns. #[ derive( Debug ) ] struct WrappedInputExtract< 'data > { + /// Tabular data for display, as `Records` only show 2 columns, we used a tuple here + /// instead of a vector. data : Vec< ( &'data str, &'data str ) >, + + /// Width of key column. key_width : usize, + + /// Width of value column. value_width : usize, } -fn wrap_text<'data> +/// Convert `InputExtract` data to properly wrapped table that is suitable for displaying. +/// `InputExtract` contains logical data of the table but it does not perform wrapping of +/// the cells (as wrapped text will be represented by new rows). +/// +/// Wrapping is controlled by `allowed_column_space` parameter. +/// `allowed_cell_space` is the size space that is allowed to be occupied by columns. +/// +/// The function will perform wrapping and shrink the columns so that they occupy not +/// more than `allowed_cell_space`. +/// +/// When you use this function, do not forget that it accepts column space, but not the +/// maximum width of the table. It means that to calculate allowed space you need to subtract +/// lengthes of visual elements (prefixes, postfixes, separators, etc.) from the maximum width. +fn text_wrap<'data> ( keys : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) >, values : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) >, @@ -254,8 +294,8 @@ fn wrap_text<'data> -> WrappedInputExtract< 'data > { let mut data = Vec::new(); - let mut key_width = calculate_width( keys ); - let mut value_width = calculate_width( values ); + let mut key_width = width_calculate( keys ); + let mut value_width = width_calculate( values ); let orig_cell_space = key_width + value_width; @@ -291,7 +331,8 @@ fn wrap_text<'data> } } -fn calculate_width< 'data > +/// Calculate how much space will a column of cells occupy without wrapping. +fn width_calculate< 'data > ( vec : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > ) diff --git a/module/core/format_tools/src/format/output_format/table.rs b/module/core/format_tools/src/format/output_format/table.rs index 1325253980..0137d000bd 100644 --- a/module/core/format_tools/src/format/output_format/table.rs +++ b/module/core/format_tools/src/format/output_format/table.rs @@ -163,8 +163,21 @@ impl Table } - /// Calculate minimum width of the table with specified numbers of columns. - pub fn calculate_minimum_width + /// Calculate how much space is 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`. + /// + /// Is is the sum of: + /// - Length of `row_prefix`. + /// - Length of `row_postfix`. + /// - Length of `cell_prefix` and `cell_postfix` multiplied by column count. + /// - Length of `cell_separator` multiplied by `column_count - 1`. + /// - Count of columns (multiplied by 1, because at least one cell should be available to render + /// meaningful information). + /// + /// 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, @@ -197,8 +210,6 @@ impl TableOutputFormat for Table + 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() }; - let minimum_acceptable_width = column_count + unchangable_width; - if self.max_width != 0 && ( unchangable_width + column_count > self.max_width ) { return Err( fmt::Error ); @@ -269,14 +280,47 @@ impl TableOutputFormat for Table } } +/// 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 height (as they are dependent on wrapping, too). +/// +/// This struct is similar to `output_format::Records::WrappedInputExtract` (which is +/// private, too), but it is suited for tabular data with several columns. #[ derive( Debug ) ] 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. data: Vec< Vec< WrappedCell< 'data > > >, + + /// New widthes of columns that include wrapping. col_widthes : Vec< usize >, + + /// Size of the first row of the table. + /// This parameter is used in case header of the table should be displayed. 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 will be included in the output formatter. #[ derive( Debug ) ] struct WrappedCell< 'data > { @@ -284,6 +328,20 @@ struct WrappedCell< 'data > content : Cow< 'data, str > } +/// Convert `InputExtract` data to properly wrapped table that is suitable for displaying. +/// `InputExtract` contains logical data of the table but it does not perform wrapping of +/// the cells (as wrapped text will be represented by new rows). +/// +/// Wrapping is controlled by `limit_column_space` and `orig_column_space` parameters. +/// `orig_column_space` is the size occupied column widthes of original tabular data. +/// `limit_column_space` is the size space that is allowed to be occupied by columns. +/// +/// The function will perform wrapping and shrink the columns so that they occupy not +/// more than `limit_column_space`. +/// +/// When you use this function, do not forget that it accepts column space, but not the +/// maximum width of the table. It means that to calculate allowed space you need to subtract +/// lengthes of visual elements (prefixes, postfixes, separators, etc.) from the maximum width. fn wrap_text< 'data > ( x : &'data InputExtract< 'data >, diff --git a/module/core/format_tools/src/format/print.rs b/module/core/format_tools/src/format/print.rs index 19fc771f22..aada1e5425 100644 --- a/module/core/format_tools/src/format/print.rs +++ b/module/core/format_tools/src/format/print.rs @@ -346,13 +346,14 @@ mod private { if self.has_header { - self.data[0][icol].0.borrow() + 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> ( @@ -368,7 +369,7 @@ 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, { diff --git a/module/core/format_tools/src/format/string.rs b/module/core/format_tools/src/format/string.rs index 1cf3986af0..ee34e9e718 100644 --- a/module/core/format_tools/src/format/string.rs +++ b/module/core/format_tools/src/format/string.rs @@ -84,35 +84,6 @@ mod private [ width, height ] } - pub fn size_with_limit< S : AsRef< str > > - ( - src : S, - limit_width : usize, - ) - -> [ usize; 2 ] - { - if limit_width == 0 - { - return size( src ); - } - - let text = src.as_ref(); - let mut height = 0; - let mut width = 0; - - for line in lines_with_limit( text, limit_width ) - { - height += 1; - let line_length = line.len(); - if line_length > width - { - width = line_length; - } - } - - [ width, height ] - } - /// Returns an iterator over the lines of a string slice. /// /// This function provides an iterator that yields each line of the input string slice. @@ -143,6 +114,37 @@ 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, @@ -212,6 +214,15 @@ 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 > { @@ -284,7 +295,6 @@ pub mod own pub use private:: { size, - size_with_limit, lines, Lines, lines_with_limit, 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 62387eb1f1..77b8de7364 100644 --- a/module/core/format_tools/tests/inc/format_records_test.rs +++ b/module/core/format_tools/tests/inc/format_records_test.rs @@ -5,11 +5,9 @@ use the_module:: { AsTable, WithRef, - Fields, filter, print, output_format, - string, }; use std:: @@ -323,7 +321,9 @@ fn filter_row_callback() #[ test ] fn test_width_limiting() { - for width in calculate_minimum_width()..calculate_maximum_width() + use the_module::string; + + for width in min_width()..max_width() { println!("width: {}", width); @@ -334,7 +334,7 @@ fn test_width_limiting() format.max_width = width; let mut output = String::new(); - let mut printer = print::Printer::with_format( &format ); + 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 ); @@ -362,7 +362,7 @@ fn test_width_limiting() fn test_error_on_unsatisfiable_limit() { // 0 is a special value that signifies no limit. - for width in 1..( calculate_minimum_width() ) + for width in 1..( min_width() ) { println!( "width: {}", width ); @@ -385,7 +385,9 @@ fn test_error_on_unsatisfiable_limit() #[ test ] fn test_table_not_grows() { - let expected_width = calculate_maximum_width(); + use the_module::string; + + let expected_width = max_width(); // The upper bound was chosen arbitrarily. for width in ( expected_width + 1 )..500 @@ -421,16 +423,18 @@ fn test_table_not_grows() /// Utility function for calculating minimum table width with `test_objects_gen()` with /// the default table style. -fn calculate_minimum_width() -> usize +fn min_width() -> usize { let format = output_format::Records::default(); - format.calculate_minimum_width() + 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 calculate_maximum_width() -> usize +fn max_width() -> usize { + use the_module::string; + let test_objects = test_object::test_objects_gen(); let as_table = AsTable::new( &test_objects ); 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 861a19e8d6..ed4620f5ea 100644 --- a/module/core/format_tools/tests/inc/format_table_test.rs +++ b/module/core/format_tools/tests/inc/format_table_test.rs @@ -5,11 +5,9 @@ use the_module:: { AsTable, WithRef, - Fields, filter, print, output_format, - string }; use std:: @@ -351,7 +349,9 @@ fn no_subtract_with_overflow() #[ test ] fn test_width_limiting() { - for width in calculate_minimum_width()..calculate_maximum_width() + use the_module::string; + + for width in min_width()..max_width() { println!("width: {}", width); @@ -380,7 +380,7 @@ fn test_width_limiting() fn test_error_on_unsatisfiable_limit() { // 0 is a special value that signifies no limit. - for width in 1..( calculate_minimum_width() ) + for width in 1..( min_width() ) { println!( "width: {}", width ); @@ -403,7 +403,9 @@ fn test_error_on_unsatisfiable_limit() #[ test ] fn test_table_not_grows() { - let expected_width = calculate_maximum_width(); + use the_module::string; + + let expected_width = max_width(); // The upper bound was chosen arbitrarily. for width in ( expected_width + 1 )..500 @@ -433,19 +435,23 @@ fn test_table_not_grows() /// Utility function for calculating minimum table width with `test_objects_gen()` with /// the default table style. -fn calculate_minimum_width() -> usize +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.calculate_minimum_width( col_count ) + format.min_width( col_count ) } /// Utility function for calculating default table width with `test_objects_gen()` with -/// the default table style with table width limit equals to 0. -fn calculate_maximum_width() -> usize +/// 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 ); From 67a6ff548da7ad07e2d7222c2318fe15d790a4fe Mon Sep 17 00:00:00 2001 From: InAnYan Date: Tue, 19 Nov 2024 11:11:04 +0200 Subject: [PATCH 09/12] Rework documentation and naming --- .../src/format/output_format/records.rs | 76 ++++++++--------- .../src/format/output_format/table.rs | 83 +++++++++---------- .../tests/inc/format_table_test.rs | 22 ++--- 3 files changed, 85 insertions(+), 96 deletions(-) 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 de09187e8c..73e71be0ee 100644 --- a/module/core/format_tools/src/format/output_format/records.rs +++ b/module/core/format_tools/src/format/output_format/records.rs @@ -94,27 +94,18 @@ impl Records INSTANCE.get_or_init( || Records::default() ) } - /// Calculate how much space is 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`. - /// - /// Is is the sum of: - /// - Length of `row_prefix`. - /// - Length of `row_postfix`. - /// - Length of `cell_prefix` and `cell_postfix` multiplied by 2. - /// - Length of `cell_separator` - /// - Just 2. - /// - /// 2 here is used as a constant because `output_format::Records` will generate tables only with - /// two columns (key and value). + /// 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::Table` + /// `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() ) @@ -194,31 +185,31 @@ impl TableOutputFormat for Records } // 2 because there are only 2 columns: key and value. - let allowed_cell_space = if self.max_width == 0 { 0 } else { self.max_width - self.min_width() + 2 }; + let max_columns_width = if self.max_width == 0 { 0 } else { self.max_width - self.min_width() + 2 }; let field_names : Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > = x.header().collect(); write!( c.buf, "{}", self.table_prefix )?; - let mut actual_entries = 0; + let mut printed_tables_count = 0; - for ( ientry_descriptor, entry_descriptor ) in x.row_descriptors.iter().enumerate() + for ( itable_descriptor, table_descriptor ) in x.row_descriptors.iter().enumerate() { - if !entry_descriptor.vis || ( x.has_header && ientry_descriptor == 0 ) + if !table_descriptor.vis || ( x.has_header && itable_descriptor == 0 ) { continue; } - if actual_entries > 0 + if printed_tables_count > 0 { write!( c.buf, "{}", self.table_separator )?; } - actual_entries += 1; + printed_tables_count += 1; - writeln!( c.buf, " = {}", entry_descriptor.irow )?; + writeln!( c.buf, " = {}", table_descriptor.irow )?; - let wrapped_text = text_wrap( &field_names, &x.data[ ientry_descriptor ], allowed_cell_space ); + let wrapped_text = text_wrap( &field_names, &x.data[ itable_descriptor ], max_columns_width ); for ( irow, ( key, value ) ) in wrapped_text.data.iter().enumerate() { @@ -261,8 +252,8 @@ impl TableOutputFormat for Records #[ derive( Debug ) ] struct WrappedInputExtract< 'data > { - /// Tabular data for display, as `Records` only show 2 columns, we used a tuple here - /// instead of a vector. + /// Tabular data for display. Because `Records` only show 2 columns, we used a tuple + /// here instead of a vector. data : Vec< ( &'data str, &'data str ) >, /// Width of key column. @@ -272,24 +263,29 @@ struct WrappedInputExtract< 'data > value_width : usize, } -/// Convert `InputExtract` data to properly wrapped table that is suitable for displaying. -/// `InputExtract` contains logical data of the table but it does not perform wrapping of -/// the cells (as wrapped text will be represented by new rows). +/// Wrap cells in `InputExtract`. +/// +/// `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). /// -/// Wrapping is controlled by `allowed_column_space` parameter. -/// `allowed_cell_space` is the size space that is allowed to be occupied by columns. +/// Wrapping is controlled by `max_columns_width` parameter. +/// `max_columns_width` is the size space that is allowed to be occupied by columns. +/// It equals to maximum table width minus lengthes of visual elements (prefixes, +/// postfixes, separators, etc.). /// /// The function will perform wrapping and shrink the columns so that they occupy not -/// more than `allowed_cell_space`. +/// more than `max_columns_width`. /// -/// When you use this function, do not forget that it accepts column space, but not the -/// maximum width of the table. It means that to calculate allowed space you need to subtract -/// lengthes of visual elements (prefixes, postfixes, separators, etc.) from the maximum width. +/// If `max_columns_width` is equal to 0, then no wrapping will be performed. fn text_wrap<'data> ( keys : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) >, values : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) >, - allowed_cell_space : usize, + max_columns_width : usize, ) -> WrappedInputExtract< 'data > { @@ -297,13 +293,13 @@ fn text_wrap<'data> let mut key_width = width_calculate( keys ); let mut value_width = width_calculate( values ); - let orig_cell_space = key_width + value_width; + let orig_columns_width = key_width + value_width; - if allowed_cell_space != 0 && orig_cell_space > allowed_cell_space + if max_columns_width != 0 && orig_columns_width > max_columns_width { - let factor = ( allowed_cell_space as f32 ) / ( orig_cell_space as f32 ); + let factor = ( max_columns_width as f32 ) / ( orig_columns_width as f32 ); key_width = ( ( key_width as f32 ) * factor ).round() as usize; - value_width = allowed_cell_space - key_width; + value_width = max_columns_width - key_width; } for i in 0..values.len() @@ -331,14 +327,14 @@ fn text_wrap<'data> } } -/// Calculate how much space will a column of cells occupy without wrapping. +/// Calculate how much width a column of cells occupy without wrapping. fn width_calculate< 'data > ( - vec : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > + column : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > ) -> usize { - vec.iter().map( |k| + column.iter().map( |k| { string::lines( k.0.as_ref() ).map( |l| l.chars().count() ).max().unwrap_or( 0 ) } ).max().unwrap_or( 0 ) diff --git a/module/core/format_tools/src/format/output_format/table.rs b/module/core/format_tools/src/format/output_format/table.rs index 0137d000bd..510b4668f2 100644 --- a/module/core/format_tools/src/format/output_format/table.rs +++ b/module/core/format_tools/src/format/output_format/table.rs @@ -163,17 +163,9 @@ impl Table } - /// Calculate how much space is needed in order to generate a table output with the specified + /// 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`. - /// - /// Is is the sum of: - /// - Length of `row_prefix`. - /// - Length of `row_postfix`. - /// - Length of `cell_prefix` and `cell_postfix` multiplied by column count. - /// - Length of `cell_separator` multiplied by `column_count - 1`. - /// - Count of columns (multiplied by 1, because at least one cell should be available to render - /// meaningful information). + /// `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. @@ -205,47 +197,42 @@ impl TableOutputFormat for Table let column_count = x.col_descriptors.len(); - let unchangable_width = 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() }; - - if self.max_width != 0 && ( unchangable_width + column_count > self.max_width ) + if self.max_width != 0 && ( self.min_width( column_count ) > self.max_width ) { return Err( fmt::Error ); } - let orig_column_space = x.col_descriptors.iter().map( |c| c.width ).sum::(); + let orig_columns_width = x.col_descriptors.iter().map( |c| c.width ).sum::(); + let visual_elements_width = self.min_width( column_count ) - column_count; - let wrapped_text = wrap_text( &x, if self.max_width == 0 { 0 } else { self.max_width - unchangable_width }, orig_column_space ); + let wrapped_text = wrap_text( &x, if self.max_width == 0 { 0 } else { self.max_width - visual_elements_width }, orig_columns_width ); - let new_column_space = wrapped_text.col_widthes.iter().sum::(); + let new_columns_widthes = wrapped_text.col_widthes.iter().sum::(); + let new_row_width = new_columns_widthes + visual_elements_width; - let new_row_width = new_column_space + unchangable_width; - - let mut actual_rows = 0; + let mut printed_row_count = 0; for row in wrapped_text.data.iter() { - if actual_rows == wrapped_text.first_row_height && x.has_header && self.delimitting_header + 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 actual_rows > 0 + if printed_row_count > 0 { write!( c.buf, "{}", row_separator )?; } - actual_rows += 1; + printed_row_count += 1; write!( c.buf, "{}", row_prefix )?; for ( icol, col ) in row.iter().enumerate() { - let cell_width = col.wrap_width; - let col_width = wrapped_text.col_widthes[ icol ]; + let cell_wrapped_width = col.wrap_width; + let column_width = wrapped_text.col_widthes[ icol ]; let slice_width = col.content.chars().count(); if icol > 0 @@ -255,8 +242,8 @@ impl TableOutputFormat for Table write!( c.buf, "{}", cell_prefix )?; - let lspaces = ( col_width - cell_width ) / 2; - let rspaces = ( ( col_width - cell_width ) as f32 / 2 as f32 ).round() as usize + cell_width - slice_width; + 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; if lspaces > 0 { @@ -320,7 +307,7 @@ struct WrappedInputExtract< 'data > /// /// 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 will be included in the output formatter. +/// right side will be included by the output formatter. #[ derive( Debug ) ] struct WrappedCell< 'data > { @@ -328,25 +315,31 @@ struct WrappedCell< 'data > content : Cow< 'data, str > } -/// Convert `InputExtract` data to properly wrapped table that is suitable for displaying. -/// `InputExtract` contains logical data of the table but it does not perform wrapping of -/// the cells (as wrapped text will be represented by new rows). +/// Wrap cells in `InputExtract`. +/// +/// `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). /// -/// Wrapping is controlled by `limit_column_space` and `orig_column_space` parameters. -/// `orig_column_space` is the size occupied column widthes of original tabular data. -/// `limit_column_space` is the size space that is allowed to be occupied by columns. +/// Wrapping is controlled by `max_columns_width` and `orig_columns_width` parameters. +/// `max_columns_width` is the size space that is allowed to be occupied by columns. +/// It equals to maximum table width minus lengthes of visual elements (prefixes, +/// postfixes, separators, etc.). +/// `orig_columns_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 `limit_column_space`. +/// more than `max_columns_width`. /// -/// When you use this function, do not forget that it accepts column space, but not the -/// maximum width of the table. It means that to calculate allowed space you need to subtract -/// lengthes of visual elements (prefixes, postfixes, separators, etc.) from the maximum width. +/// If `max_columns_width` is equal to 0, then no wrapping will be performed. fn wrap_text< 'data > ( x : &'data InputExtract< 'data >, - limit_column_space : usize, - orig_column_space : usize, + max_columns_width : usize, + orig_columns_width : usize, ) -> WrappedInputExtract< 'data > { @@ -354,13 +347,13 @@ fn wrap_text< 'data > let mut new_data = Vec::new(); let mut col_widthes = Vec::new(); - if limit_column_space == 0 || limit_column_space >= orig_column_space + if max_columns_width == 0 || max_columns_width >= orig_columns_width { col_widthes.extend( x.col_descriptors.iter().map( |d| d.width ) ); } else { - let shrink_factor: f32 = ( limit_column_space as f32 ) / ( orig_column_space as f32 ); + let shrink_factor : f32 = ( max_columns_width as f32 ) / ( orig_columns_width as f32 ); for ( icol, col ) in x.col_descriptors.iter().enumerate() { @@ -371,7 +364,7 @@ fn wrap_text< 'data > let col_width_to_put = if icol == x.col_descriptors.len() - 1 { - limit_column_space - col_widthes.iter().sum::() + max_columns_width - col_widthes.iter().sum::() } else { 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 ed4620f5ea..945696f572 100644 --- a/module/core/format_tools/tests/inc/format_table_test.rs +++ b/module/core/format_tools/tests/inc/format_table_test.rs @@ -351,15 +351,15 @@ fn test_width_limiting() { use the_module::string; - for width in min_width()..max_width() + for max_width in min_width()..max_width() { - println!("width: {}", 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 = width; + format.max_width = max_width; let mut output = String::new(); let printer = print::Printer::with_format( &format ); @@ -371,7 +371,7 @@ fn test_width_limiting() for line in string::lines( &output ) { - assert_eq!( width, line.chars().count() ); + assert_eq!( max_width, line.chars().count() ); } } } @@ -379,16 +379,16 @@ fn test_width_limiting() #[ test ] fn test_error_on_unsatisfiable_limit() { - // 0 is a special value that signifies no limit. - for width in 1..( min_width() ) + // 0 is a special value that signifies no limit. Therefore, the lower bound is 1. + for max_width in 1..( min_width() ) { - println!( "width: {}", 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 = width; + format.max_width = max_width; let mut output = String::new(); let printer = print::Printer::with_format( &format ); @@ -408,15 +408,15 @@ fn test_table_not_grows() let expected_width = max_width(); // The upper bound was chosen arbitrarily. - for width in ( expected_width + 1 )..500 + for max_width in ( expected_width + 1 )..500 { - println!( "width: {}", 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 = width; + format.max_width = max_width; let mut output = String::new(); let printer = print::Printer::with_format( &format ); From 35e22f41ba590c0f7b64b8b14c8d1566e847595f Mon Sep 17 00:00:00 2001 From: InAnYan Date: Tue, 19 Nov 2024 12:24:20 +0200 Subject: [PATCH 10/12] Fix docs --- .../src/format/output_format/records.rs | 30 ++++++++--------- .../src/format/output_format/table.rs | 32 +++++++++---------- 2 files changed, 30 insertions(+), 32 deletions(-) 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 73e71be0ee..7930fd5263 100644 --- a/module/core/format_tools/src/format/output_format/records.rs +++ b/module/core/format_tools/src/format/output_format/records.rs @@ -185,7 +185,7 @@ impl TableOutputFormat for Records } // 2 because there are only 2 columns: key and value. - let max_columns_width = if self.max_width == 0 { 0 } else { self.max_width - self.min_width() + 2 }; + let columns_max_width = if self.max_width == 0 { 0 } else { self.max_width - self.min_width() + 2 }; let field_names : Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > = x.header().collect(); @@ -209,7 +209,7 @@ impl TableOutputFormat for Records writeln!( c.buf, " = {}", table_descriptor.irow )?; - let wrapped_text = text_wrap( &field_names, &x.data[ itable_descriptor ], max_columns_width ); + let wrapped_text = text_wrap( &field_names, &x.data[ itable_descriptor ], columns_max_width ); for ( irow, ( key, value ) ) in wrapped_text.data.iter().enumerate() { @@ -266,26 +266,24 @@ struct WrappedInputExtract< 'data > /// Wrap cells in `InputExtract`. /// /// `InputExtract` contains cells with full content, so it represents the logical -/// structure of the table. +/// 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). /// -/// `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). -/// -/// Wrapping is controlled by `max_columns_width` parameter. -/// `max_columns_width` is the size space that is allowed to be occupied by columns. +/// Wrapping is controlled by `columns_max_width` parameter. +/// `columns_max_width` is the size space that is allowed to be occupied by columns. /// It equals to maximum table width minus lengthes of visual elements (prefixes, /// postfixes, separators, etc.). /// /// The function will perform wrapping and shrink the columns so that they occupy not -/// more than `max_columns_width`. +/// more than `columns_max_width`. /// -/// If `max_columns_width` is equal to 0, then no wrapping will be performed. +/// If `columns_max_width` is equal to 0, then no wrapping will be performed. fn text_wrap<'data> ( keys : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) >, values : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) >, - max_columns_width : usize, + columns_max_width : usize, ) -> WrappedInputExtract< 'data > { @@ -295,11 +293,11 @@ fn text_wrap<'data> let orig_columns_width = key_width + value_width; - if max_columns_width != 0 && orig_columns_width > max_columns_width + if columns_max_width != 0 && orig_columns_width > columns_max_width { - let factor = ( max_columns_width as f32 ) / ( orig_columns_width as f32 ); + let factor = ( columns_max_width as f32 ) / ( orig_columns_width as f32 ); key_width = ( ( key_width as f32 ) * factor ).round() as usize; - value_width = max_columns_width - key_width; + value_width = columns_max_width - key_width; } for i in 0..values.len() @@ -327,7 +325,7 @@ fn text_wrap<'data> } } -/// Calculate how much width a column of cells occupy without wrapping. +/// Calculate width of the column without wrapping. fn width_calculate< 'data > ( column : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > diff --git a/module/core/format_tools/src/format/output_format/table.rs b/module/core/format_tools/src/format/output_format/table.rs index 510b4668f2..9fe33752c8 100644 --- a/module/core/format_tools/src/format/output_format/table.rs +++ b/module/core/format_tools/src/format/output_format/table.rs @@ -202,10 +202,10 @@ impl TableOutputFormat for Table return Err( fmt::Error ); } - let orig_columns_width = x.col_descriptors.iter().map( |c| c.width ).sum::(); + 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 wrapped_text = wrap_text( &x, if self.max_width == 0 { 0 } else { self.max_width - visual_elements_width }, orig_columns_width ); + let wrapped_text = wrap_text( &x, if self.max_width == 0 { 0 } else { self.max_width - visual_elements_width }, columns_nowrap_width ); let new_columns_widthes = wrapped_text.col_widthes.iter().sum::(); let new_row_width = new_columns_widthes + visual_elements_width; @@ -318,28 +318,28 @@ struct WrappedCell< 'data > /// Wrap cells in `InputExtract`. /// /// `InputExtract` contains cells with full content, so it represents the logical -/// structure of the table. +/// 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). /// -/// `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). +/// Wrapping is controlled by `columns_max_width` and `columns_nowrap_width` parameters. /// -/// Wrapping is controlled by `max_columns_width` and `orig_columns_width` parameters. -/// `max_columns_width` is the size space that is allowed to be occupied by columns. +/// - `columns_max_width` is the size space that is allowed to be occupied by columns. /// It equals to maximum table width minus lengthes of visual elements (prefixes, /// postfixes, separators, etc.). -/// `orig_columns_width` is the sum of column widthes of cells without wrapping (basically, +/// +/// - `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 `max_columns_width`. +/// more than `columns_max_width`. /// -/// If `max_columns_width` is equal to 0, then no wrapping will be performed. +/// If `columns_max_width` is equal to 0, then no wrapping will be performed. fn wrap_text< 'data > ( x : &'data InputExtract< 'data >, - max_columns_width : usize, - orig_columns_width : usize, + columns_max_width : usize, + columns_nowrap_width : usize, ) -> WrappedInputExtract< 'data > { @@ -347,13 +347,13 @@ fn wrap_text< 'data > let mut new_data = Vec::new(); let mut col_widthes = Vec::new(); - if max_columns_width == 0 || max_columns_width >= orig_columns_width + if columns_max_width == 0 || columns_max_width >= columns_nowrap_width { col_widthes.extend( x.col_descriptors.iter().map( |d| d.width ) ); } else { - let shrink_factor : f32 = ( max_columns_width as f32 ) / ( orig_columns_width as f32 ); + let shrink_factor : f32 = ( columns_max_width as f32 ) / ( columns_nowrap_width as f32 ); for ( icol, col ) in x.col_descriptors.iter().enumerate() { @@ -364,7 +364,7 @@ fn wrap_text< 'data > let col_width_to_put = if icol == x.col_descriptors.len() - 1 { - max_columns_width - col_widthes.iter().sum::() + columns_max_width - col_widthes.iter().sum::() } else { From 3189379fe0545bcfb7193622b251ce4d491b2b3b Mon Sep 17 00:00:00 2001 From: InAnYan Date: Fri, 22 Nov 2024 10:06:38 +0200 Subject: [PATCH 11/12] Move out `text_wrap` from `table` --- module/core/format_tools/src/format.rs | 4 + .../src/format/output_format/records.rs | 2 +- .../src/format/output_format/table.rs | 194 ++------------- .../core/format_tools/src/format/text_wrap.rs | 231 ++++++++++++++++++ 4 files changed, 257 insertions(+), 174 deletions(-) create mode 100644 module/core/format_tools/src/format/text_wrap.rs 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 7930fd5263..06f08abf06 100644 --- a/module/core/format_tools/src/format/output_format/records.rs +++ b/module/core/format_tools/src/format/output_format/records.rs @@ -244,7 +244,7 @@ impl TableOutputFormat for Records /// 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 height (as they are dependent on wrapping, too). +/// widthes and height (as they are dependent on wrapping too). /// /// This struct is similar to `output_format::Table::WrappedInputExtract` (which is /// private, too), but it is made only for 2 columns, as tables in `Records` contain diff --git a/module/core/format_tools/src/format/output_format/table.rs b/module/core/format_tools/src/format/output_format/table.rs index 9fe33752c8..9670a11100 100644 --- a/module/core/format_tools/src/format/output_format/table.rs +++ b/module/core/format_tools/src/format/output_format/table.rs @@ -16,7 +16,6 @@ use print:: InputExtract, Context, }; -use std::borrow::Cow; use core:: { fmt, @@ -187,6 +186,8 @@ 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; @@ -205,7 +206,25 @@ impl TableOutputFormat for Table 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 wrapped_text = wrap_text( &x, if self.max_width == 0 { 0 } else { self.max_width - visual_elements_width }, columns_nowrap_width ); + 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(), + if self.max_width == 0 { 0 } else { self.max_width - visual_elements_width }, + columns_nowrap_width + ); let new_columns_widthes = wrapped_text.col_widthes.iter().sum::(); let new_row_width = new_columns_widthes + visual_elements_width; @@ -265,175 +284,4 @@ impl TableOutputFormat for Table Ok(()) } -} - -/// 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 height (as they are dependent on wrapping, too). -/// -/// This struct is similar to `output_format::Records::WrappedInputExtract` (which is -/// private, too), but it is suited for tabular data with several columns. -#[ derive( Debug ) ] -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. - data: Vec< Vec< WrappedCell< 'data > > >, - - /// New widthes of columns that include wrapping. - col_widthes : Vec< usize >, - - /// Size of the first row of the table. - /// This parameter is used in case header of the table should be displayed. - 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 will be included by the output formatter. -#[ derive( Debug ) ] -struct WrappedCell< 'data > -{ - wrap_width : usize, - content : Cow< 'data, str > -} - -/// Wrap cells in `InputExtract`. -/// -/// `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). -/// -/// Wrapping is controlled by `columns_max_width` and `columns_nowrap_width` parameters. -/// -/// - `columns_max_width` is the size space 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. -fn wrap_text< 'data > -( - x : &'data InputExtract< 'data >, - columns_max_width : usize, - columns_nowrap_width : usize, -) --> WrappedInputExtract< 'data > -{ - let mut first_row_height = 0; - let mut new_data = Vec::new(); - let mut col_widthes = Vec::new(); - - if columns_max_width == 0 || columns_max_width >= columns_nowrap_width - { - col_widthes.extend( x.col_descriptors.iter().map( |d| d.width ) ); - } - else - { - let shrink_factor : f32 = ( columns_max_width as f32 ) / ( columns_nowrap_width as f32 ); - - for ( icol, col ) in x.col_descriptors.iter().enumerate() - { - let col_width = col.width; - - 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 == x.col_descriptors.len() - 1 - { - columns_max_width - col_widthes.iter().sum::() - } - else - { - col_limit.max(1) - }; - - col_widthes.push( col_width_to_put ); - } - } - - for ( irow, row ) in x.data.iter().enumerate() - { - let row_descriptor = &x.row_descriptors[ irow ]; - - if !row_descriptor.vis - { - continue; - } - - let mut wrapped_rows : Vec< Vec< Cow< 'data, str > > > = vec![]; - - for ( icol, col ) in row.iter().enumerate() - { - let col_limit = col_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, - col_widthes - } } \ No newline at end of file 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..8ca66de8e0 --- /dev/null +++ b/module/core/format_tools/src/format/text_wrap.rs @@ -0,0 +1,231 @@ +//! +//! 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 col_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_vec` generated by `InputExtract`. + /// + /// Notice: data passed to this function should contain only visible rows and columns. + /// + /// 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_vec : Vec< usize >, + columns_max_width : usize, + columns_nowrap_width : usize, + ) + -> WrappedInputExtract< 'data > + { + let mut first_row_height = 0; + let mut new_data = Vec::new(); + let mut col_widthes = Vec::new(); + + if columns_max_width == 0 || columns_max_width >= columns_nowrap_width + { + col_widthes.extend( columns_width_vec.iter() ); + } + else + { + let shrink_factor : f32 = ( columns_max_width as f32 ) / ( columns_nowrap_width as f32 ); + + for ( icol, col_width ) in columns_width_vec.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_vec.len() - 1 + { + columns_max_width - col_widthes.iter().sum::() + } + else + { + col_limit.max(1) + }; + + col_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 = col_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, + col_widthes + } + } + +} + +#[ 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, + }; + +} + +/// 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::*; +} From ec9e321e34e4275ad83de024239f0a4a8dc04269 Mon Sep 17 00:00:00 2001 From: InAnYan Date: Fri, 22 Nov 2024 10:34:49 +0200 Subject: [PATCH 12/12] Make `Records` use general `text_wrap` --- .../src/format/output_format/records.rs | 133 ++++-------------- .../src/format/output_format/table.rs | 6 +- .../core/format_tools/src/format/text_wrap.rs | 51 +++++-- 3 files changed, 71 insertions(+), 119 deletions(-) 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 06f08abf06..1c89f34038 100644 --- a/module/core/format_tools/src/format/output_format/records.rs +++ b/module/core/format_tools/src/format/output_format/records.rs @@ -179,6 +179,8 @@ 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 ); @@ -187,7 +189,8 @@ impl TableOutputFormat for Records // 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 field_names : Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > = x.header().collect(); + let keys : Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > = x.header().collect(); + let keys_width = width_calculate( &keys ); write!( c.buf, "{}", self.table_prefix )?; @@ -209,26 +212,46 @@ impl TableOutputFormat for Records writeln!( c.buf, " = {}", table_descriptor.irow )?; - let wrapped_text = text_wrap( &field_names, &x.data[ itable_descriptor ], columns_max_width ); + let values = &x.data[ itable_descriptor ]; + let values_width = width_calculate( &values ); - for ( irow, ( key, value ) ) in wrapped_text.data.iter().enumerate() + let table_for_wrapping : Vec< Vec< ( Cow< 'data, str >, [ usize; 2] ) > > = + keys.iter().enumerate().map( | ( ikey, key ) | + { + vec![ key.clone(), values[ ikey ].clone() ] + }).collect(); + + 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 { write!( c.buf, "{}", self.row_separator )?; } - let key_width = wrapped_text.key_width; - let value_width = wrapped_text.value_width; + let key = &cols[ 0 ]; + let value = &cols[ 1 ]; + + let key_width = wrapped_text.column_widthes[ 0 ]; + let value_width = wrapped_text.column_widthes[ 1 ]; write!( c.buf, "{}", self.row_prefix )?; write!( c.buf, "{}", self.cell_prefix )?; - write!( c.buf, "{: -{ - /// Tabular data for display. Because `Records` only show 2 columns, we used a tuple - /// here instead of a vector. - data : Vec< ( &'data str, &'data str ) >, - - /// Width of key column. - key_width : usize, - - /// Width of value column. - value_width : usize, -} - -/// Wrap cells in `InputExtract`. -/// -/// `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). -/// -/// Wrapping is controlled by `columns_max_width` parameter. -/// `columns_max_width` is the size space that is allowed to be occupied by columns. -/// It equals to maximum table width minus lengthes of visual elements (prefixes, -/// postfixes, separators, etc.). -/// -/// 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. -fn text_wrap<'data> -( - keys : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) >, - values : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) >, - columns_max_width : usize, -) --> WrappedInputExtract< 'data > -{ - let mut data = Vec::new(); - let mut key_width = width_calculate( keys ); - let mut value_width = width_calculate( values ); - - let orig_columns_width = key_width + value_width; - - if columns_max_width != 0 && orig_columns_width > columns_max_width - { - let factor = ( columns_max_width as f32 ) / ( orig_columns_width as f32 ); - key_width = ( ( key_width as f32 ) * factor ).round() as usize; - value_width = columns_max_width - key_width; - } - - for i in 0..values.len() - { - let key = &keys[ i ]; - let value = &values[ i ]; - - let key_wrapped : Vec< &'data str > = string::lines_with_limit( key.0.as_ref(), key_width ).collect(); - let value_wrapped : Vec< &'data str > = string::lines_with_limit( value.0.as_ref(), value_width ).collect(); - - for j in 0..( key_wrapped.len().max( value_wrapped.len() ) ) - { - let key = key_wrapped.get( j ).copied().unwrap_or( "" ); - let value = value_wrapped.get( j ).copied().unwrap_or( "" ); - - data.push( ( key, value ) ); - } - } - - WrappedInputExtract - { - data, - key_width, - value_width, - } -} - -/// Calculate width of the column without wrapping. -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 ) -} \ No newline at end of file diff --git a/module/core/format_tools/src/format/output_format/table.rs b/module/core/format_tools/src/format/output_format/table.rs index 9670a11100..42f2afe16f 100644 --- a/module/core/format_tools/src/format/output_format/table.rs +++ b/module/core/format_tools/src/format/output_format/table.rs @@ -221,12 +221,12 @@ impl TableOutputFormat for Table let wrapped_text = text_wrap ( filtered_data, - x.col_descriptors.iter().map( | c | c.width ).collect(), + 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.col_widthes.iter().sum::(); + 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; @@ -251,7 +251,7 @@ impl TableOutputFormat for Table for ( icol, col ) in row.iter().enumerate() { let cell_wrapped_width = col.wrap_width; - let column_width = wrapped_text.col_widthes[ icol ]; + let column_width = wrapped_text.column_widthes[ icol ]; let slice_width = col.content.chars().count(); if icol > 0 diff --git a/module/core/format_tools/src/format/text_wrap.rs b/module/core/format_tools/src/format/text_wrap.rs index 8ca66de8e0..695ac287cd 100644 --- a/module/core/format_tools/src/format/text_wrap.rs +++ b/module/core/format_tools/src/format/text_wrap.rs @@ -24,7 +24,7 @@ mod private pub data: Vec< Vec< WrappedCell< 'data > > >, /// New widthes of columns that include wrapping. - pub col_widthes : Vec< usize >, + 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. @@ -68,9 +68,18 @@ mod private /// /// `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_vec` generated by `InputExtract`. + /// in `columns_width_list` generated by `InputExtract`. /// - /// Notice: data passed to this function should contain only visible rows and columns. + /// `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. /// @@ -88,39 +97,41 @@ mod private pub fn text_wrap< 'data > ( data : impl Iterator< Item = &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > >, - columns_width_vec : Vec< usize >, + 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 col_widthes = Vec::new(); + let mut column_widthes = Vec::new(); if columns_max_width == 0 || columns_max_width >= columns_nowrap_width { - col_widthes.extend( columns_width_vec.iter() ); + 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_vec.iter().enumerate() + 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_vec.len() - 1 + let col_width_to_put = if icol == columns_width_list.len() - 1 { - columns_max_width - col_widthes.iter().sum::() + columns_max_width - column_widthes.iter().sum::() } else { col_limit.max(1) }; - col_widthes.push( col_width_to_put ); + column_widthes.push( col_width_to_put ); } } @@ -130,7 +141,7 @@ mod private for ( icol, col ) in row.iter().enumerate() { - let col_limit = col_widthes[ icol ]; + 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 ); } @@ -169,17 +180,30 @@ mod private first_row_height += transposed.len(); } - new_data.extend(transposed); + new_data.extend( transposed ); } WrappedInputExtract { data: new_data, first_row_height, - col_widthes + 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 ) ] @@ -202,6 +226,7 @@ pub mod own pub use private:: { text_wrap, + width_calculate, }; }