From 06d834e3a57abefdfd0356f464219323a2af5158 Mon Sep 17 00:00:00 2001 From: Pierre-Henri Symoneaux Date: Tue, 18 Sep 2018 10:32:31 +0200 Subject: Implemented horizontal span Updated rust compatibility Fixed UT Added span support from macro Example + updated README Small update to README --- .travis.yml | 6 ---- README.md | 27 +++++++------- examples/span.rs | 31 ++++++++++++++++ src/cell.rs | 106 +++++++++++++++++++++++++++++++++++++++---------------- src/lib.rs | 28 +++++++++++++-- src/main.rs | 4 +-- src/row.rs | 71 +++++++++++++++++++++++++++++-------- 7 files changed, 204 insertions(+), 69 deletions(-) create mode 100644 examples/span.rs diff --git a/.travis.yml b/.travis.yml index 5674fa2..f6b9da3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,5 @@ language: rust rust: -- 1.20.0 -- 1.21.0 -- 1.22.1 -- 1.23.0 -- 1.24.1 -- 1.25.0 - 1.26.2 - 1.27.2 - 1.28.0 diff --git a/README.md b/README.md index d1f9157..af660a4 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Include the library as a dependency to your project by adding the following line prettytable-rs = "^0.7" ``` -The library requires at least `rust v1.20.0`. +The library requires at least `rust v1.26.0`. ## Basic usage @@ -143,7 +143,7 @@ Rows may have different numbers of cells. The table will automatically adapt to ## Do it with style! -Tables can have a styled output with background and foreground colors, bold and italic as configurable settings, thanks to the `term` crate. +Tables can have a styled output with background and foreground colors, bold and italic as configurable settings, thanks to the `term` crate. Alignment in cells can also be set (Left, Right, Center), and a cell can span accross multiple columns. `term` style attributes are reexported @@ -155,35 +155,37 @@ Tables can have a styled output with background and foreground colors, bold and table.add_row(Row::new(vec![ Cell::new("foobar") - .with_style(Attr::Bold), - .with_style(Attr::ForegroundColor(color::GREEN)) + .with_style(Attr::Bold) + .with_style(Attr::ForegroundColor(color::GREEN)), Cell::new("bar") - .with_style(Attr::BackgroundColor(color::RED)), - .with_style(Attr::Italic(true)), - Cell::new("foo")])); + .with_style(Attr::BackgroundColor(color::RED)) + .with_style(Attr::Italic(true)) + .with_hspan(2), + Cell::new("foo") + ])); ``` - through style strings: ```rust table.add_row(Row::new(vec![ Cell::new("foobar").style_spec("bFg"), - Cell::new("bar").style_spec("Bri"), + Cell::new("bar").style_spec("BriH2"), Cell::new("foo")])); ``` - using `row!` macro: ```rust - table.add_row(row![bFg->"foobar", Bri->"bar", "foo"]); + table.add_row(row![bFg->"foobar", BriH2->"bar", "foo"]); ``` - using `table!` macro (this one creates a new table, unlike previous examples): ```rust - table!([bFg->"foobar", Bri->"bar", "foo"]); + table!([bFg->"foobar", BriH2->"bar", "foo"]); ``` Here - **bFg** means **bold**, **F**oreground: **g**reen, -- **Bri** means **B**ackground: **r**ed, **i**talic. +- **BriH2** means **B**ackground: **r**ed, **i**talic, **H**orizontal span of **2**. Another example: **FrBybc** means **F**oreground: **r**ed, **B**ackground: **y**ellow, **b**old, **c**enter. @@ -214,6 +216,7 @@ All cases of styling cells in macros: * **F** : **F**oreground (must be followed by a color specifier) * **B** : **B**ackground (must be followed by a color specifier) +* **H** : **H**orizontal span (must be followed by a number) * **b** : **b**old * **i** : **i**talic * **u** : **u**nderline @@ -375,6 +378,6 @@ Since `v0.6.3`, platform specific line endings are activated though the default When this feature is deactivated (for instance with the `--no-default-features` flag in cargo), line endings will be rendered with `\n` on any platform. -This customization capability will probably move to Formatting API in `v0.7`. +This customization capability will probably move to Formatting API in a future release. Additional examples are provided in the documentation and in [examples](./examples/) directory. diff --git a/examples/span.rs b/examples/span.rs new file mode 100644 index 0000000..61e1815 --- /dev/null +++ b/examples/span.rs @@ -0,0 +1,31 @@ +#[macro_use] +extern crate prettytable; +use prettytable::{row::Row, cell::Cell, format::Alignment}; + + +fn main() { + + /* + The following code will output + + +---------------+---------------+--------------+ + | A table with horizontal span | + +===============+===============+==============+ + | This is a cell with span of 2 | span of 1 | + +---------------+---------------+--------------+ + | span of 1 | span of 1 | span of 1 | + +---------------+---------------+--------------+ + | This cell with a span of 3 is centered | + +---------------+---------------+--------------+ + */ + + let mut table: prettytable::Table = table![ + [H2 -> "This is a cell with span of 2", "span of 1"], + ["span of 1", "span of 1", "span of 1"], + [H03c -> "This cell with a span of 3 is centered"] + ]; + table.set_titles(Row::new(vec![ + Cell::new_align("A table with horizontal span", Alignment::CENTER).with_hspan(3) + ])); + table.printstd(); +} \ No newline at end of file diff --git a/src/cell.rs b/src/cell.rs index cca22d4..de13f36 100644 --- a/src/cell.rs +++ b/src/cell.rs @@ -1,11 +1,12 @@ //! This module contains definition of table/row cells stuff -use std::io::{Write, Error}; -use std::string::ToString; -use super::{Attr, Terminal, color}; use super::format::Alignment; use super::utils::display_width; use super::utils::print_align; +use super::{color, Attr, Terminal}; +use std::io::{Error, Write}; +use std::string::ToString; +use std::str::FromStr; /// Represent a table cell containing a string. /// @@ -17,6 +18,7 @@ pub struct Cell { width: usize, align: Alignment, style: Vec, + hspan: usize, } impl Cell { @@ -36,6 +38,7 @@ impl Cell { width: width, align: align, style: Vec::new(), + hspan: 1, } } @@ -61,6 +64,12 @@ impl Cell { self } + /// Add horizontal spanning to the cell + pub fn with_hspan(mut self, hspan: usize) -> Cell { + self.set_hspan(hspan); + self + } + /// Remove all style attributes and reset alignment to default (LEFT) pub fn reset_style(&mut self) { self.style.clear(); @@ -107,7 +116,8 @@ impl Cell { self.reset_style(); let mut foreground = false; let mut background = false; - for c in spec.chars() { + let mut it = spec.chars().peekable(); + while let Some(c) = it.next() { if foreground || background { let color = match c { 'r' => color::RED, @@ -150,6 +160,14 @@ impl Cell { 'c' => self.align(Alignment::CENTER), 'l' => self.align(Alignment::LEFT), 'r' => self.align(Alignment::RIGHT), + 'H' => { + let mut span_s = String::new(); + while let Some('0'..='9') = it.peek() { + span_s.push(it.next().unwrap()); + } + let span = usize::from_str(&span_s).unwrap(); + self.set_hspan(span); + } _ => { /* Silently ignore unknown tags */ } } } @@ -167,6 +185,16 @@ impl Cell { self.width } + /// Set horizontal span for this cell (must be > 0) + pub fn set_hspan(&mut self, hspan: usize) { + self.hspan = if hspan <= 0 {1} else {hspan}; + } + + /// Get horizontal span of this cell (> 0) + pub fn get_hspan(&self) -> usize { + self.hspan + } + /// Return a copy of the full string contained in the cell pub fn get_content(&self) -> String { self.content.join("\n") @@ -176,36 +204,38 @@ impl Cell { /// `idx` is the line index to print. `col_width` is the column width used to /// fill the cells with blanks so it fits in the table. /// If `ìdx` is higher than this cell's height, it will print empty content - pub fn print(&self, - out: &mut T, - idx: usize, - col_width: usize, - skip_right_fill: bool) - -> Result<(), Error> { + pub fn print( + &self, + out: &mut T, + idx: usize, + col_width: usize, + skip_right_fill: bool, + ) -> Result<(), Error> { let c = self.content.get(idx).map(|s| s.as_ref()).unwrap_or(""); print_align(out, self.align, c, ' ', col_width, skip_right_fill) } /// Apply style then call `print` to print the cell into a terminal - pub fn print_term(&self, - out: &mut T, - idx: usize, - col_width: usize, - skip_right_fill: bool) - -> Result<(), Error> { + pub fn print_term( + &self, + out: &mut T, + idx: usize, + col_width: usize, + skip_right_fill: bool, + ) -> Result<(), Error> { for a in &self.style { match out.attr(*a) { - Ok(..) | - Err(::term::Error::NotSupported) | - Err(::term::Error::ColorOutOfRange) => (), // Ignore unsupported atrributes + Ok(..) | Err(::term::Error::NotSupported) | Err(::term::Error::ColorOutOfRange) => { + () + } // Ignore unsupported atrributes Err(e) => return Err(term_error_to_io_error(e)), }; } self.print(out, idx, col_width, skip_right_fill)?; match out.reset() { - Ok(..) | - Err(::term::Error::NotSupported) | - Err(::term::Error::ColorOutOfRange) => Ok(()), + Ok(..) | Err(::term::Error::NotSupported) | Err(::term::Error::ColorOutOfRange) => { + Ok(()) + } Err(e) => Err(term_error_to_io_error(e)), } } @@ -238,6 +268,7 @@ impl Default for Cell { width: 0, align: Alignment::LEFT, style: Vec::new(), + hspan: 1, } } } @@ -271,17 +302,23 @@ impl Default for Cell { /// ``` #[macro_export] macro_rules! cell { - () => ($crate::cell::Cell::default()); - ($value:expr) => ($crate::cell::Cell::new(&$value.to_string())); - ($style:ident -> $value:expr) => (cell!($value).style_spec(stringify!($style))); + () => { + $crate::cell::Cell::default() + }; + ($value:expr) => { + $crate::cell::Cell::new(&$value.to_string()) + }; + ($style:ident -> $value:expr) => { + cell!($value).style_spec(stringify!($style)) + }; } #[cfg(test)] mod tests { use cell::Cell; - use utils::StringWriter; use format::Alignment; - use term::{Attr, color}; + use term::{color, Attr}; + use utils::StringWriter; #[test] fn get_content() { @@ -350,14 +387,18 @@ mod tests { assert!(cell.style.contains(&Attr::Italic(true))); assert!(cell.style.contains(&Attr::Bold)); assert!(cell.style.contains(&Attr::ForegroundColor(color::RED))); - assert!(cell.style - .contains(&Attr::BackgroundColor(color::BRIGHT_BLUE))); + assert!( + cell.style + .contains(&Attr::BackgroundColor(color::BRIGHT_BLUE)) + ); assert_eq!(cell.align, Alignment::CENTER); cell = cell.style_spec("FDBwr"); assert_eq!(cell.style.len(), 2); - assert!(cell.style - .contains(&Attr::ForegroundColor(color::BRIGHT_BLACK))); + assert!( + cell.style + .contains(&Attr::ForegroundColor(color::BRIGHT_BLACK)) + ); assert!(cell.style.contains(&Attr::BackgroundColor(color::WHITE))); assert_eq!(cell.align, Alignment::RIGHT); @@ -368,6 +409,9 @@ mod tests { assert_eq!(cell.style.len(), 1); cell = cell.style_spec("zzz"); assert!(cell.style.is_empty()); + assert_eq!(cell.get_hspan(), 1); + cell = cell.style_spec("FDBwH03r"); + assert_eq!(cell.get_hspan(), 3); } #[test] diff --git a/src/lib.rs b/src/lib.rs index 906f97b..03c011a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,7 +75,7 @@ impl<'a> TableSlice<'a> { pub fn get_column_num(&self) -> usize { let mut cnum = 0; for r in self.rows { - let l = r.len(); + let l = r.column_count(); if l > cnum { cnum = l; } @@ -102,11 +102,11 @@ impl<'a> TableSlice<'a> { /// Return 0 if the column does not exists; fn get_column_width(&self, col_idx: usize) -> usize { let mut width = match *self.titles { - Some(ref t) => t.get_cell_width(col_idx), + Some(ref t) => t.get_column_width(col_idx, self.format), None => 0, }; for r in self.rows { - let l = r.get_cell_width(col_idx); + let l = r.get_column_width(col_idx, self.format); if l > width { width = l; } @@ -120,6 +120,7 @@ impl<'a> TableSlice<'a> { let colnum = self.get_column_num(); let mut col_width = vec![0usize; colnum]; for i in 0..colnum { + // TODO: calling "get_column_width()" in a loop is inefficient col_width[i] = self.get_column_width(i); } col_width @@ -1030,6 +1031,27 @@ mod tests { assert_eq!(out, table.to_string().replace("\r\n","\n")); } + #[test] + fn test_horizontal_span() { + let mut table = Table::new(); + table.set_titles(Row::new(vec![Cell::new("t1"), Cell::new("t2").with_hspan(2)])); + table.add_row(Row::new(vec![Cell::new("a"), Cell::new("bc"), Cell::new("def")])); + table.add_row(Row::new(vec![Cell::new("def").style_spec("H02c"), Cell::new("a")])); + let out = "\ ++----+----+-----+ +| t1 | t2 | ++====+====+=====+ +| a | bc | def | ++----+----+-----+ +| def | a | ++----+----+-----+ +"; + println!("{}", out); + println!("____"); + println!("{}", table.to_string().replace("\r\n","\n")); + assert_eq!(out, table.to_string().replace("\r\n","\n")); + } + #[cfg(feature = "csv")] mod csv { use Table; diff --git a/src/main.rs b/src/main.rs index 8251340..c653413 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use prettytable::{Attr, color}; fn main() { let _ = table!(); let mut table = Table::new(); - table.add_row(row![FrByb->"ABC", "DEFG", "HIJKLMN"]); + table.add_row(row![FrByH2b->"This is a long spanning cell", "DEFG", "HIJKLMN"]); table.add_row(row!["foobar", "bar", "foo"]); table.add_row(row![]); // Add style to a full row @@ -21,7 +21,7 @@ fn main() { table.add_row(Row::new(vec![ Cell::new("foobar2"), // Create a cell with a red foreground color - Cell::new("bar2").with_style(Attr::ForegroundColor(color::RED)), + Cell::new_align("bar2", Alignment::CENTER).with_style(Attr::ForegroundColor(color::RED)).with_hspan(2), // Create a cell with red foreground color, yellow background color, with bold characters Cell::new("foo2").style_spec("FrByb")]) ); diff --git a/src/row.rs b/src/row.rs index f02f9ef..a604dc7 100644 --- a/src/row.rs +++ b/src/row.rs @@ -28,9 +28,17 @@ impl Row { Self::new(vec![Cell::default(); 0]) } + /// Count the number of column required in the table grid. + /// It takes into account horizontal spanning of cells. For + /// example, a cell with an hspan of 3 will add 3 column to the grid + pub fn column_count(&self) -> usize { + self.cells.iter().map(|c| c.get_hspan()).sum() + } + /// Get the number of cells in this row pub fn len(&self) -> usize { self.cells.len() + // self.cells.iter().map(|c| c.get_hspan()).sum() } /// Check if the row is empty (has no cell) @@ -52,11 +60,31 @@ impl Row { /// Get the minimum width required by the cell in the column `column`. /// Return 0 if the cell does not exist in this row - pub fn get_cell_width(&self, column: usize) -> usize { - self.cells - .get(column) - .map(|cell| cell.get_width()) - .unwrap_or(0) + pub fn get_column_width(&self, column: usize, format: &TableFormat) -> usize { + let mut i = 0; + for c in &self.cells { + if i + c.get_hspan()-1 >= column { + if c.get_hspan() == 1 { + return c.get_width(); + } + let (lp, rp) = format.get_padding(); + let sep = format.get_column_separator(ColumnPosition::Intern).map(|_| 1).unwrap_or_default(); + let rem = lp + rp +sep; + let mut w = c.get_width(); + if w > rem { + w -= rem; + } else { + w = 0; + } + return (w as f64 / c.get_hspan() as f64).ceil() as usize; + } + i += c.get_hspan(); + } + 0 + // self.cells + // .get(column) + // .map(|cell| cell.get_width()) + // .unwrap_or(0) } /// Get the cell at index `idx` @@ -69,12 +97,12 @@ impl Row { self.cells.get_mut(idx) } - /// Set the `cell` in the row at the given `column` - pub fn set_cell(&mut self, cell: Cell, column: usize) -> Result<(), &str> { - if column >= self.len() { + /// Set the `cell` in the row at the given `idx` index + pub fn set_cell(&mut self, cell: Cell, idx: usize) -> Result<(), &str> { + if idx >= self.len() { return Err("Cannot find cell"); } - self.cells[column] = cell; + self.cells[idx] = cell; Ok(()) } @@ -124,18 +152,31 @@ impl Row { out.write_all(&vec![b' '; format.get_indent()])?; format.print_column_separator(out, ColumnPosition::Left)?; let (lp, rp) = format.get_padding(); - for j in 0..col_width.len() { - out.write_all(&vec![b' '; lp])?; + let mut j = 0; + let mut hspan = 0; // The additional offset caused by cell's horizontal spanning + while j+hspan < col_width.len() { + out.write_all(&vec![b' '; lp])?; // Left padding + // skip_r_fill skip filling the end of the last cell if there's no character + // delimiting the end of the table let skip_r_fill = (j == col_width.len() - 1) && format.get_column_separator(ColumnPosition::Right).is_none(); match self.get_cell(j) { - Some(c) => f(c, out, i, col_width[j], skip_r_fill)?, - None => f(&Cell::default(), out, i, col_width[j], skip_r_fill)?, + Some(c) => { + // In case of horizontal spanning, width is the sum of all spanned columns' width + let mut w = col_width[j+hspan..j+hspan+c.get_hspan()].iter().sum(); + let real_span = c.get_hspan()-1; + w += real_span * (lp + rp) + real_span * format.get_column_separator(ColumnPosition::Intern).map(|_| 1).unwrap_or_default(); + // Print cell content + f(c, out, i, w, skip_r_fill)?; + hspan += real_span; // Add span to offset + }, + None => f(&Cell::default(), out, i, col_width[j+hspan], skip_r_fill)?, }; - out.write_all(&vec![b' '; rp])?; - if j < col_width.len() - 1 { + out.write_all(&vec![b' '; rp])?; // Right padding + if j+hspan < col_width.len() - 1 { format.print_column_separator(out, ColumnPosition::Intern)?; } + j+=1; } format.print_column_separator(out, ColumnPosition::Right)?; out.write_all(NEWLINE)?; -- cgit v1.2.3