Repository: Nukesor/comfy-table
Pull request #17 (2020-11-27 21:58:16)

Custom delimiter logic
This allows to set custom delimiters, which are used to split the text into consistent pieces.

Mentioned in Issue #16 


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ce84737..84664a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [1.4.0] - 
+
+### Added
+
+- Allow to set custom delimiters on tables, columns and cells.
+
 ## [1.3.0] - 2020-11-20
 
 ### Added
diff --git a/src/cell.rs b/src/cell.rs
index d95c119..a019c3f 100644
--- a/src/cell.rs
+++ b/src/cell.rs
@@ -9,6 +9,9 @@ pub struct Cell {
     /// This is done to handle newlines more easily.
     /// On set_content, the incoming string is split by '\n'
     pub(crate) content: Vec<String>,
+    /// The delimiter which is used to split the text into consistent pieces.
+    /// The default is ` `.
+    pub(crate) delimiter: Option<char>,
     pub(crate) alignment: Option<CellAlignment>,
     pub(crate) fg: Option<Color>,
     pub(crate) bg: Option<Color>,
@@ -24,6 +27,7 @@ impl Cell {
                 .split('\n')
                 .map(|content| content.to_string())
                 .collect(),
+            delimiter: None,
             alignment: None,
             fg: None,
             bg: None,
@@ -36,6 +40,13 @@ impl Cell {
         self.content.join("\n")
     }
 
+    /// Set the delimiter used to split text for this cell
+    pub fn set_delimiter(mut self, delimiter: char) -> Self {
+        self.delimiter = Some(delimiter);
+
+        self
+    }
+
     /// Set the alignment of content for this cell.
     ///
     /// Setting this overwrites alignment settings of the Column for this specific Cell.
diff --git a/src/column.rs b/src/column.rs
index 8464eb5..ee7939b 100644
--- a/src/column.rs
+++ b/src/column.rs
@@ -32,6 +32,9 @@ pub struct Column {
     pub index: usize,
     /// Left/right padding for each cell of this column in spaces
     pub(crate) padding: (u16, u16),
+    /// The delimiter which is used to split the text into consistent pieces.
+    /// Default is ` `.
+    pub(crate) delimiter: Option<char>,
     /// Define the cell alligment for all cells of this column
     pub(crate) cell_alignment: Option<CellAlignment>,
     pub(crate) max_content_width: u16,
@@ -43,6 +46,7 @@ impl Column {
         Column {
             index,
             padding: (1, 1),
+            delimiter: None,
             constraint: None,
             max_content_width: 0,
             cell_alignment: None,
@@ -58,6 +62,15 @@ impl Column {
         self
     }
 
+    /// Set the delimiter used to split text for this column's cells.
+    /// A custom delimiter on a cell in will overwrite the column's delimiter.
+    /// The default is a simple space ` `.
+    pub fn set_delimiter(&mut self, delimiter: char) -> &mut Self {
+        self.delimiter = Some(delimiter);
+
+        self
+    }
+
     /// Get the width in characters of the widest line in this column.
     /// This doesn't include padding yet!
     pub fn get_max_content_width(&self) -> u16 {
diff --git a/src/table.rs b/src/table.rs
index 18ff199..fa5f597 100644
--- a/src/table.rs
+++ b/src/table.rs
@@ -23,6 +23,7 @@ pub struct Table {
     pub(crate) header: Option<Row>,
     pub(crate) rows: Vec<Row>,
     pub(crate) arrangement: ContentArrangement,
+    pub(crate) delimiter: Option<char>,
     no_tty: bool,
     table_width: Option<u16>,
     enforce_styling: bool,
@@ -52,6 +53,7 @@ impl Table {
             header: None,
             rows: Vec::new(),
             arrangement: ContentArrangement::Disabled,
+            delimiter: None,
             no_tty: false,
             table_width: None,
             style: HashMap::new(),
@@ -142,6 +144,15 @@ impl Table {
         self
     }
 
+    /// Set the delimiter used to split text in all cells.
+    /// A custom delimiter on a cell in will overwrite the column's delimiter.
+    /// The default is a simple space ` `.
+    pub fn set_delimiter(&mut self, delimiter: char) -> &mut Self {
+        self.delimiter = Some(delimiter);
+
+        self
+    }
+
     /// In case you are sure you don't want export tables to a tty
     /// or you experience problems with tty specific code, you can
     /// enforce a non_tty mode.
@@ -199,6 +210,7 @@ impl Table {
     /// If more Constraints are passed than there are Columns, these Constraints will be ignored
     /// ```
     /// use comfy_table::{Table, ColumnConstraint, ContentArrangement};
+    ///
     /// let mut table = Table::new();
     /// table.add_row(&vec!["one", "two", "three"])
     ///     .set_content_arrangement(ContentArrangement::Dynamic)
@@ -258,13 +270,13 @@ impl Table {
     /// A pure convenience method, so you're not force to fiddle with those preset strings.
     ///
     /// ```
-    ///    use comfy_table::Table;
-    ///    use comfy_table::presets::UTF8_FULL;
+    /// use comfy_table::Table;
+    /// use comfy_table::presets::UTF8_FULL;
     ///
-    ///    let mut table = Table::new();
-    ///    table.load_preset(UTF8_FULL);
+    /// let mut table = Table::new();
+    /// table.load_preset(UTF8_FULL);
     ///
-    ///    assert_eq!(UTF8_FULL, table.current_style_as_preset())
+    /// assert_eq!(UTF8_FULL, table.current_style_as_preset())
     /// ```
     pub fn current_style_as_preset(&mut self) -> String {
         let components = TableComponent::iter();
@@ -321,7 +333,7 @@ impl Table {
     /// For example, if `TopBorderIntersections` is `None` the first row would look like this:
     /// ```text
     /// +------ ------+
-    /// | asdf | ghij |
+    /// | this | test |
     /// ```
     ///
     /// If in addition `TopLeftCorner`,`TopBorder` and `TopRightCorner` would be `None` as well,
diff --git a/src/utils/arrangement.rs b/src/utils/arrangement.rs
index a9994b4..f30a884 100644
--- a/src/utils/arrangement.rs
+++ b/src/utils/arrangement.rs
@@ -15,6 +15,7 @@ use crate::utils::borders::{
 #[derive(Debug)]
 pub struct ColumnDisplayInfo {
     pub padding: (u16, u16),
+    pub delimiter: Option<char>,
     /// The max amount of characters over all lines in this column
     max_content_width: u16,
     /// The actual allowed content width after arrangement
@@ -32,6 +33,7 @@ impl ColumnDisplayInfo {
     fn new(column: &Column) -> Self {
         ColumnDisplayInfo {
             padding: column.padding,
+            delimiter: column.delimiter,
             max_content_width: column.max_content_width,
             content_width: 0,
             fixed: false,
@@ -93,7 +95,7 @@ pub(crate) fn arrange_content(table: &Table) -> Vec<ColumnDisplayInfo> {
     for column in table.columns.iter() {
         let mut info = ColumnDisplayInfo::new(column);
 
-        if let Some(constraint) = column.constraint.as_ref() {
+        if let Some(constraint) = column.constraint {
             evaluate_constraint(&mut info, constraint, table_width);
         }
 
@@ -119,7 +121,7 @@ pub(crate) fn arrange_content(table: &Table) -> Vec<ColumnDisplayInfo> {
 /// Look at given constraints of a column and populate the ColumnDisplayInfo depending on those.
 fn evaluate_constraint(
     info: &mut ColumnDisplayInfo,
-    constraint: &ColumnConstraint,
+    constraint: ColumnConstraint,
     table_width: Option<u16>,
 ) {
     match constraint {
@@ -128,23 +130,23 @@ fn evaluate_constraint(
             info.fixed = true;
         }
         Width(width) => {
-            let width = info.without_padding(*width);
+            let width = info.without_padding(width);
             info.set_content_width(width);
             info.fixed = true;
         }
         MinWidth(min_width) => {
             // In case a min_width is specified, we can already fix the size of the column
             // right now (since we already know the max_content_width.
-            if info.max_width() <= *min_width {
-                let width = info.without_padding(*min_width);
+            if info.max_width() <= min_width {
+                let width = info.without_padding(min_width);
                 info.set_content_width(width);
                 info.fixed = true;
             }
         }
-        MaxWidth(max_width) => info.constraint = Some(MaxWidth(*max_width)),
+        MaxWidth(max_width) => info.constraint = Some(MaxWidth(max_width)),
         Percentage(percent) => {
             if let Some(table_width) = table_width {
-                let mut width = (table_width as i32 * *percent as i32 / 100) as u16;
+                let mut width = (table_width as i32 * percent as i32 / 100) as u16;
                 width = info.without_padding(width as u16);
                 info.set_content_width(width);
                 info.fixed = true;
@@ -152,7 +154,7 @@ fn evaluate_constraint(
         }
         MinPercentage(percent) => {
             if let Some(table_width) = table_width {
-                let min_width = (table_width as i32 * *percent as i32 / 100) as u16;
+                let min_width = (table_width as i32 * percent as i32 / 100) as u16;
                 if info.max_width() <= min_width {
                     let width = info.without_padding(min_width);
                     info.set_content_width(width);
@@ -162,7 +164,7 @@ fn evaluate_constraint(
         }
         MaxPercentage(percent) => {
             if let Some(table_width) = table_width {
-                let max_width = (table_width as i32 * *percent as i32 / 100) as u16;
+                let max_width = (table_width as i32 * percent as i32 / 100) as u16;
                 info.constraint = Some(MaxWidth(max_width));
             }
         }
diff --git a/src/utils/format.rs b/src/utils/format.rs
index bb6335e..62f8840 100644
--- a/src/utils/format.rs
+++ b/src/utils/format.rs
@@ -67,11 +67,22 @@ pub fn format_row(
             continue;
         };
 
+        // Determine, which delimiter should be used
+        let delimiter = if let Some(delimiter) = cell.delimiter {
+            delimiter
+        } else if let Some(delimiter) = info.delimiter {
+            delimiter
+        } else if let Some(delimiter) = table.delimiter {
+            delimiter
+        } else {
+            ' '
+        };
+
         // Iterate over each line and split it into multiple lines, if necessary.
         // Newlines added by the user will be preserved.
         for line in cell.content.iter() {
             if (line.chars().count() as u16) > info.content_width() {
-                let mut splitted = split_line(&line, &info);
+                let mut splitted = split_line(&line, &info, delimiter);
                 cell_lines.append(&mut splitted);
             } else {
                 cell_lines.push(line.into());
diff --git a/src/utils/split.rs b/src/utils/split.rs
index 4e41eaf..31e5c1c 100644
--- a/src/utils/split.rs
+++ b/src/utils/split.rs
@@ -12,10 +12,9 @@ use crate::utils::arrangement::ColumnDisplayInfo;
 /// This is repeated until there're no more "elements".
 ///
 /// Mid-element splits only occurs if a element doesn't fit in a single line by itself.
-pub fn split_line(line: &str, info: &ColumnDisplayInfo) -> Vec<String> {
+pub fn split_line(line: &str, info: &ColumnDisplayInfo, delimiter: char) -> Vec<String> {
     let mut lines = Vec::new();
     let content_width = info.content_width();
-    let delimiter = ' ';
 
     // Split the line by the given deliminator and turn the content into a stack.
     // Reverse it, since we want to push/pop without reversing the text.
@@ -88,7 +87,7 @@ pub fn split_line(line: &str, info: &ColumnDisplayInfo) -> Vec<String> {
             let mut next: Vec<char> = next.chars().collect();
             // Only add delimiter, if we're not on a fresh line
             if !current_line.is_empty() {
-                current_line += " ";
+                current_line.push(delimiter);
             }
 
             let remaining = next.split_off((remaining_width) as usize);


Test patch:
diff --git a/tests/custom_delimiter_test.rs b/tests/custom_delimiter_test.rs
new file mode 100644
index 0000000..31aad25
--- /dev/null
+++ b/tests/custom_delimiter_test.rs
@@ -0,0 +1,60 @@
+use pretty_assertions::assert_eq;
+
+use comfy_table::*;
+
+#[test]
+/// Create a table with a custom delimiter on Table, Column and Cell level.
+/// The first column should be splitted with the table's delimiter.
+/// The first cell of the second column should be split with the custom column delimiter
+/// The second cell of the second column should be split with the custom cell delimiter
+fn full_custom_delimiters() {
+    let mut table = Table::new();
+
+    table
+        .set_header(&vec!["Header1", "Header2"])
+        .set_content_arrangement(ContentArrangement::Dynamic)
+        .set_delimiter('-')
+        .set_table_width(40)
+        .add_row(&vec![
+            "This shouldn't be split with any logic, since there's no matching delimiter",
+            "Test-Test-Test-Test-Test-This_should_only_be_splitted_by_underscore_and not by space or hyphens",
+        ]);
+
+    // Give the bottom right cell a special delimiter
+    table.add_row(vec![
+        Cell::new("Test_Test_Test_Test_Test_This-should-only-be-splitted-by-hyphens-not by space or underscore",),
+        Cell::new(
+            "Test-Test-Test-Test-Test-Test_Test_Test_Test_Test_Test_Test_This/should/only/be/splitted/by/backspace/and not by space or hyphens or anything else.",
+        )
+        .set_delimiter('/'),
+    ]);
+
+    let column = table.get_column_mut(1).unwrap();
+    column.set_delimiter('_');
+
+    println!("{}", table.to_string());
+    let expected = "
++-------------------+------------------+
+| Header1           | Header2          |
++======================================+
+| This shouldn't be | Test-Test-Test-T |
+|  split with any l | est-Test-This    |
+| ogic, since there | should_only_be   |
+| 's no matching de | splitted_by      |
+| limiter           | underscore_and n |
+|                   | ot by space or h |
+|                   | yphens           |
+|-------------------+------------------|
+| Test_Test_Test_Te | Test-Test-Test-T |
+| st_Test_This      | est-Test-Test_Te |
+| should-only-be    | st_Test_Test_Tes |
+| splitted-by       | t_Test_Test_This |
+| hyphens-not by sp | should/only/be   |
+| ace or underscore | splitted/by      |
+|                   | backspace/and no |
+|                   | t by space or hy |
+|                   | phens or anythin |
+|                   | g else.          |
++-------------------+------------------+";
+    assert_eq!("\n".to_string() + &table.to_string(), expected);
+}