diff --git a/.gitignore b/.gitignore index 469f38c..25e521e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ target pom.xml.* release.properties + +.settings/ +.project/ +.classpath/ diff --git a/README.md b/README.md index 524db0d..45035e3 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,52 @@ System.out.println(FlipTableConverters.fromObjects(headers, data)); ╚════════════╧═══════════╧═════╧═════════╝ ``` +Column wrapping (optional) +-------------------------- +**Fixed Table width** +```java +String[] headers = { "First Name", "Last Name", "Details" }; +String[][] data = { + { "One One One One", "Two Two Two:Two", "Three Three.Three,Three" }, + { "Joe", "Smith", "Hello" } +}; +System.out.println(FlipTable.of(headers, data, FixedWidth.withWidth(30))); + +``` +``` +╔════════════╤═══════════╤═════════════╗ +║ First Name │ Last Name │ Details ║ +╠════════════╪═══════════╪═════════════╣ +║ One One │ Two Two │ Three Three ║ +║ One One │ Two:Two │ .Three, ║ +║ │ │ Three ║ +╟────────────┼───────────┼─────────────╢ +║ Joe │ Smith │ Hello ║ +╚════════════╧═══════════╧═════════════╝ +``` + +**Custom Column widths** +```java +String[] headers = { "First", "Last", "Det" }; +String[][] data = { + { "One One One One", "Two Two Two:Two", "Fifteen four on on on on on five" }, + { "Joe", "Boe", "Hello" } +}; +System.out.println(FlipTable.of(headers, data, CustomColumnWidth.withColumnWidths(new int[] {5, 5, 8}))); +``` +``` +╔═══════╤═══════╤══════════╗ +║ First │ Last │ Det ║ +╠═══════╪═══════╪══════════╣ +║ One │ Two │ Fifteen ║ +║ One │ Two │ four on ║ +║ One │ Two: │ on on on ║ +║ One │ Two │ on five ║ +╟───────┼───────┼──────────╢ +║ Joe │ Boe │ Hello ║ +╚═══════╧═══════╧══════════╝ +``` Download -------- diff --git a/pom.xml b/pom.xml index bf1c695..999fe1f 100644 --- a/pom.xml +++ b/pom.xml @@ -68,8 +68,8 @@ maven-compiler-plugin 3.1 - 1.7 - 1.7 + 1.8 + 1.8 diff --git a/src/main/java/com/jakewharton/fliptables/FlipTable.java b/src/main/java/com/jakewharton/fliptables/FlipTable.java index 4407ce3..37ecdd7 100644 --- a/src/main/java/com/jakewharton/fliptables/FlipTable.java +++ b/src/main/java/com/jakewharton/fliptables/FlipTable.java @@ -15,6 +15,10 @@ */ package com.jakewharton.fliptables; +import com.jakewharton.fliptables.format.wrap.ColumnWrapFormat; +import com.jakewharton.fliptables.format.wrap.DefaultWrapFormat; +import com.jakewharton.fliptables.format.wrap.WrappedTableData; + /** *
  * ╔═════════════╤════════════════════════════╤══════════════╗
@@ -30,10 +34,13 @@ public final class FlipTable {
 
   /** Create a new table with the specified headers and row data. */
   public static String of(String[] headers, String[][] data) {
+	  return of(headers, data, new DefaultWrapFormat());
+  }
+  public static String of(String[] headers, String[][] data, ColumnWrapFormat columnWrapFormat) {
     if (headers == null) throw new NullPointerException("headers == null");
     if (headers.length == 0) throw new IllegalArgumentException("Headers must not be empty.");
     if (data == null) throw new NullPointerException("data == null");
-    return new FlipTable(headers, data).toString();
+    return new FlipTable(headers, data, columnWrapFormat).toString();
   }
 
   private final String[] headers;
@@ -42,12 +49,10 @@ public static String of(String[] headers, String[][] data) {
   private final int[] columnWidths;
   private final int emptyWidth;
 
-  private FlipTable(String[] headers, String[][] data) {
-    this.headers = headers;
-    this.data = data;
-
+  private FlipTable(String[] headers, String[][] data, ColumnWrapFormat columnWrapFormat) {
+    
     columns = headers.length;
-    columnWidths = new int[columns];
+    int[] columnWidths = new int[columns];
     for (int row = -1; row < data.length; row++) {
       String[] rowData = (row == -1) ? headers : data[row]; // Hack to parse headers too.
       if (rowData.length != columns) {
@@ -61,6 +66,11 @@ private FlipTable(String[] headers, String[][] data) {
         }
       }
     }
+    
+    WrappedTableData wrappedTableData = columnWrapFormat.adjustData(headers, data, columnWidths);
+    this.headers = wrappedTableData.getHeaders();
+    this.data = wrappedTableData.getData();
+    this.columnWidths = wrappedTableData.getColumnWidths();
 
     int emptyWidth = 3 * (columns - 1); // Account for column dividers and their spacing.
     for (int columnWidth : columnWidths) {
diff --git a/src/main/java/com/jakewharton/fliptables/format/wrap/ColumnWrapFormat.java b/src/main/java/com/jakewharton/fliptables/format/wrap/ColumnWrapFormat.java
new file mode 100644
index 0000000..555a68b
--- /dev/null
+++ b/src/main/java/com/jakewharton/fliptables/format/wrap/ColumnWrapFormat.java
@@ -0,0 +1,53 @@
+package com.jakewharton.fliptables.format.wrap;
+
+/**
+ * Provides a mechanism to wrap the text of the columns.
+ * Line breaks are added to break long texts without breaking the words
+ */
+public abstract class ColumnWrapFormat {
+	
+	private static final String SPLITTER_REGEX = "((?=:|,|\\.|\\s)|(?<=:|,|\\.|\\s))";
+	private static final String LINE_BREAK = "\n";
+	
+	public abstract WrappedTableData adjustData(String[] headers, String[][] data, int[] columnWidths);
+	
+	protected String[][] adjustedData(String[][] data, int[] adjustedWidths) {
+		String[][] adjustedData = new String[data.length][adjustedWidths.length];
+		for(int row = 0; row < data.length; row++) {
+			for(int col = 0; col < adjustedWidths.length; col++) {
+				adjustedData[row][col] = adjustFieldData(data[row][col], adjustedWidths[col]);
+			}
+		}
+		return adjustedData;
+	}
+	
+	protected String adjustFieldData(String field, int desiredWidth) {
+		if(null == field || field.isEmpty() || field.length() <= desiredWidth) {
+			return field;
+		}
+		return withLineBreaks(field, desiredWidth);
+	}
+	
+	/**
+	 * Adds line breaks on maxLineLength boundaries without breaking the words
+	 */
+	private String withLineBreaks(String input, int maxLineLength) {
+		String[] components = input.split(SPLITTER_REGEX);
+		StringBuilder builder = new StringBuilder();
+		int lineLength = 0, iterator = 0;
+		do {
+			String component = components[iterator]; 
+			if(lineLength == 0 || (lineLength + component.length() <= maxLineLength)) {
+				builder.append(component);
+				lineLength += component.length();
+				iterator++;
+			}
+			else {
+				builder.append(LINE_BREAK);
+				lineLength = 0;
+			}
+		} while(iterator < components.length);
+		
+		return builder.toString();
+	}
+}
diff --git a/src/main/java/com/jakewharton/fliptables/format/wrap/CustomColumnWidth.java b/src/main/java/com/jakewharton/fliptables/format/wrap/CustomColumnWidth.java
new file mode 100644
index 0000000..bed8b72
--- /dev/null
+++ b/src/main/java/com/jakewharton/fliptables/format/wrap/CustomColumnWidth.java
@@ -0,0 +1,31 @@
+package com.jakewharton.fliptables.format.wrap;
+
+/**
+ * Adjusting the columns of the table to the user provided column widths
+ * A column is never made narrower than the length of the header
+ */
+public class CustomColumnWidth extends ColumnWrapFormat {
+	
+	private int[] desiredColumnWidths;
+	
+	public static CustomColumnWidth withColumnWidths(int[] desiredColumnWidths) {
+		return new CustomColumnWidth(desiredColumnWidths);
+	}
+	
+	private CustomColumnWidth(int[] desiredColumnWidths) {
+		this.desiredColumnWidths = desiredColumnWidths;
+	}
+	
+	@Override
+	public WrappedTableData adjustData(String[] headers, String[][] data, int[] columnWidths) {
+		if (desiredColumnWidths.length != headers.length) {
+			throw new IllegalArgumentException("Length of the array of the desired columns does not match the number of columns");
+		}
+		
+		int[] adjustedWidths = new int[columnWidths.length];
+		for(int col = 0; col < columnWidths.length; col++) {
+			adjustedWidths[col] = Math.max(headers[col].length(), desiredColumnWidths[col]);
+		}
+		return new WrappedTableData(headers, adjustedData(data, adjustedWidths), adjustedWidths);
+	}
+}
diff --git a/src/main/java/com/jakewharton/fliptables/format/wrap/DefaultWrapFormat.java b/src/main/java/com/jakewharton/fliptables/format/wrap/DefaultWrapFormat.java
new file mode 100644
index 0000000..cdd5d59
--- /dev/null
+++ b/src/main/java/com/jakewharton/fliptables/format/wrap/DefaultWrapFormat.java
@@ -0,0 +1,9 @@
+package com.jakewharton.fliptables.format.wrap;
+
+public class DefaultWrapFormat extends ColumnWrapFormat {
+
+	@Override
+	public WrappedTableData adjustData(String[] headers, String[][] data, int[] columnWidths) {
+		return new WrappedTableData(headers, data, columnWidths);
+	}
+}
diff --git a/src/main/java/com/jakewharton/fliptables/format/wrap/FixedWidth.java b/src/main/java/com/jakewharton/fliptables/format/wrap/FixedWidth.java
new file mode 100644
index 0000000..afd042e
--- /dev/null
+++ b/src/main/java/com/jakewharton/fliptables/format/wrap/FixedWidth.java
@@ -0,0 +1,43 @@
+package com.jakewharton.fliptables.format.wrap;
+
+import java.util.Arrays;
+
+/**
+ * Reduces the individual column widths proportional to the table width.
+ * A column is never made narrower than the length of the header
+ */
+public class FixedWidth extends ColumnWrapFormat {
+	
+	private int maxWidth;
+	
+	public static FixedWidth withWidth(int tableWidth) {
+		return new FixedWidth(tableWidth);
+	}
+	
+	private FixedWidth(int width) {
+		this.maxWidth = width;
+	}
+	
+	@Override
+	public WrappedTableData adjustData(String[] headers, String[][] data, int[] columnWidths) {
+		int currentLength = Arrays.stream(columnWidths).sum();
+		double ratio = maxWidth / (1.0 * currentLength);
+		if(ratio > 1.0d) {
+			return new WrappedTableData(headers, data, columnWidths);
+		}
+		int[] adjustedWidths = adjustedColumnWidths(headers, columnWidths, ratio);
+		return new WrappedTableData(headers, adjustedData(data, adjustedWidths), adjustedWidths);
+	}
+	
+	private int[] adjustedColumnWidths(String headers[], int[] columnWidths, double ratio) {
+		int[] adjustedWidths = headers.length == 1 ?  new int[] { maxWidth }: new int[headers.length];
+		int index = 0, sum = 0;
+		for(; index < headers.length - 1; index++) {
+			adjustedWidths[index] = Math.max((int) (ratio * columnWidths[index]), headers[index].length());
+			sum += adjustedWidths[index];
+		}
+		adjustedWidths[index] = Math.max(maxWidth - sum, headers[index].length());
+		return adjustedWidths;
+	}
+
+}
diff --git a/src/main/java/com/jakewharton/fliptables/format/wrap/WrappedTableData.java b/src/main/java/com/jakewharton/fliptables/format/wrap/WrappedTableData.java
new file mode 100644
index 0000000..ee91c12
--- /dev/null
+++ b/src/main/java/com/jakewharton/fliptables/format/wrap/WrappedTableData.java
@@ -0,0 +1,30 @@
+package com.jakewharton.fliptables.format.wrap;
+
+/**
+ * Class to hold the adjusted (wrapped) data out
+ */
+public class WrappedTableData {
+	String[] headers;
+	String[][] data;
+	int[] columnWidths;
+	
+	public WrappedTableData() {}
+	
+	public WrappedTableData(String[] headers, String[][] data, int[] columnWidths) {
+		this.headers = headers;
+		this.data = data;
+		this.columnWidths = columnWidths;
+	}
+	
+	public String[] getHeaders() {
+		return this.headers;
+	}
+	
+	public String[][] getData() {
+		return this.data;
+	}
+	
+	public int[] getColumnWidths() {
+		return this.columnWidths;
+	}
+}
diff --git a/src/test/java/com/jakewharton/fliptables/format/wrap/ColumnWrapTest.java b/src/test/java/com/jakewharton/fliptables/format/wrap/ColumnWrapTest.java
new file mode 100644
index 0000000..57d3d66
--- /dev/null
+++ b/src/test/java/com/jakewharton/fliptables/format/wrap/ColumnWrapTest.java
@@ -0,0 +1,58 @@
+package com.jakewharton.fliptables.format.wrap;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.junit.Test;
+
+import com.jakewharton.fliptables.FlipTable;
+import com.jakewharton.fliptables.util.ResourceReader;
+
+public class ColumnWrapTest {
+	
+	private String[] headers = new String[] {"Feather", "Weather", "Fizz", "Buzz"};
+	private String[][] data = new String[3][headers.length];
+	
+	public ColumnWrapTest() {
+		for(int idx = 0; idx < 3; idx++) {
+			data[idx][0] = columnData("a", 6,  10);
+			data[idx][1] = columnData("b", 6,  5);
+			data[idx][2] = columnData("c", 6,  15);
+			data[idx][3] = columnData("d", 6,  25);
+		}
+		data[1][2] = "some long words somelongwords somelongerwords someevenlongerwords someevenmorelongerwords";
+	}
+	
+	private static String columnData(String base, int wordSize, int words) {
+		return IntStream.range(0, words)
+			.mapToObj(wordIndex -> IntStream.range(0, wordSize).mapToObj(charIndex -> base).collect(Collectors.joining()))
+			.collect(Collectors.joining(" "));
+	}
+	
+	@Test
+	public void testFixedWidthWrapping() throws IOException {
+		String read = ResourceReader.readFromResourceFile("fixed-width-wrapping.txt");
+		String flipTable = FlipTable.of(headers, data, FixedWidth.withWidth(120));
+		assertThat(flipTable).isEqualTo(read);
+	}
+	
+	@Test
+	public void testCustomWidthWrapping() throws IOException {
+		String read = ResourceReader.readFromResourceFile("custom-width-wrapping.txt");
+		String flipTable = FlipTable.of(headers, data, CustomColumnWidth.withColumnWidths(new int[] {30, 30, 30, 30}));
+		assertThat(flipTable).isEqualTo(read);
+	}
+	
+	public static void main(String args[]) {
+		String[] headers = { "First", "Last", "Det" };
+		String[][] data = {
+		    { "One One One One", "Two Two Two:Two", "Fifteen four on on on on on five" },
+		    { "Joe", "Boe", "Hello" }
+		};
+		System.out.println(FlipTable.of(headers, data, CustomColumnWidth.withColumnWidths(new int[] {5, 5, 8})));
+	}
+
+}
diff --git a/src/test/java/com/jakewharton/fliptables/util/ResourceReader.java b/src/test/java/com/jakewharton/fliptables/util/ResourceReader.java
new file mode 100644
index 0000000..64193ab
--- /dev/null
+++ b/src/test/java/com/jakewharton/fliptables/util/ResourceReader.java
@@ -0,0 +1,26 @@
+package com.jakewharton.fliptables.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+public class ResourceReader {
+	
+	public static String readFromResourceFile(String filePath) throws IOException {
+		
+		try(InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath)) {
+		    StringBuilder textBuilder = new StringBuilder();
+		    try (Reader reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName(StandardCharsets.UTF_8.name())))) {
+		        int c = 0;
+		        while ((c = reader.read()) != -1) {
+		            textBuilder.append((char) c);
+		        }
+		    }
+		    return textBuilder.toString();
+		}
+	}
+}
diff --git a/src/test/resources/custom-width-wrapping.txt b/src/test/resources/custom-width-wrapping.txt
new file mode 100644
index 0000000..7f7055d
--- /dev/null
+++ b/src/test/resources/custom-width-wrapping.txt
@@ -0,0 +1,27 @@
+╔════════════════════════════════╤════════════════════════════════╤════════════════════════════════╤════════════════════════════════╗
+║ Feather                        │ Weather                        │ Fizz                           │ Buzz                           ║
+╠════════════════════════════════╪════════════════════════════════╪════════════════════════════════╪════════════════════════════════╣
+║ aaaaaa aaaaaa aaaaaa aaaaaa    │ bbbbbb bbbbbb bbbbbb bbbbbb    │ cccccc cccccc cccccc cccccc    │ dddddd dddddd dddddd dddddd    ║
+║ aaaaaa aaaaaa aaaaaa aaaaaa    │ bbbbbb                         │ cccccc cccccc cccccc cccccc    │ dddddd dddddd dddddd dddddd    ║
+║ aaaaaa aaaaaa                  │                                │ cccccc cccccc cccccc cccccc    │ dddddd dddddd dddddd dddddd    ║
+║                                │                                │ cccccc cccccc cccccc           │ dddddd dddddd dddddd dddddd    ║
+║                                │                                │                                │ dddddd dddddd dddddd dddddd    ║
+║                                │                                │                                │ dddddd dddddd dddddd dddddd    ║
+║                                │                                │                                │ dddddd                         ║
+╟────────────────────────────────┼────────────────────────────────┼────────────────────────────────┼────────────────────────────────╢
+║ aaaaaa aaaaaa aaaaaa aaaaaa    │ bbbbbb bbbbbb bbbbbb bbbbbb    │ some long words somelongwords  │ dddddd dddddd dddddd dddddd    ║
+║ aaaaaa aaaaaa aaaaaa aaaaaa    │ bbbbbb                         │ somelongerwords                │ dddddd dddddd dddddd dddddd    ║
+║ aaaaaa aaaaaa                  │                                │ someevenlongerwords            │ dddddd dddddd dddddd dddddd    ║
+║                                │                                │ someevenmorelongerwords        │ dddddd dddddd dddddd dddddd    ║
+║                                │                                │                                │ dddddd dddddd dddddd dddddd    ║
+║                                │                                │                                │ dddddd dddddd dddddd dddddd    ║
+║                                │                                │                                │ dddddd                         ║
+╟────────────────────────────────┼────────────────────────────────┼────────────────────────────────┼────────────────────────────────╢
+║ aaaaaa aaaaaa aaaaaa aaaaaa    │ bbbbbb bbbbbb bbbbbb bbbbbb    │ cccccc cccccc cccccc cccccc    │ dddddd dddddd dddddd dddddd    ║
+║ aaaaaa aaaaaa aaaaaa aaaaaa    │ bbbbbb                         │ cccccc cccccc cccccc cccccc    │ dddddd dddddd dddddd dddddd    ║
+║ aaaaaa aaaaaa                  │                                │ cccccc cccccc cccccc cccccc    │ dddddd dddddd dddddd dddddd    ║
+║                                │                                │ cccccc cccccc cccccc           │ dddddd dddddd dddddd dddddd    ║
+║                                │                                │                                │ dddddd dddddd dddddd dddddd    ║
+║                                │                                │                                │ dddddd dddddd dddddd dddddd    ║
+║                                │                                │                                │ dddddd                         ║
+╚════════════════════════════════╧════════════════════════════════╧════════════════════════════════╧════════════════════════════════╝
diff --git a/src/test/resources/fixed-width-wrapping.txt b/src/test/resources/fixed-width-wrapping.txt
new file mode 100644
index 0000000..611c55e
--- /dev/null
+++ b/src/test/resources/fixed-width-wrapping.txt
@@ -0,0 +1,21 @@
+╔═══════════════════════╤════════════╤══════════════════════════════════╤═══════════════════════════════════════════════════════════╗
+║ Feather               │ Weather    │ Fizz                             │ Buzz                                                      ║
+╠═══════════════════════╪════════════╪══════════════════════════════════╪═══════════════════════════════════════════════════════════╣
+║ aaaaaa aaaaaa aaaaaa  │ bbbbbb     │ cccccc cccccc cccccc cccccc      │ dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd   ║
+║ aaaaaa aaaaaa aaaaaa  │ bbbbbb     │ cccccc cccccc cccccc cccccc      │ dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd   ║
+║ aaaaaa aaaaaa aaaaaa  │ bbbbbb     │ cccccc cccccc cccccc cccccc      │ dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd   ║
+║ aaaaaa                │ bbbbbb     │ cccccc cccccc cccccc             │ dddddd                                                    ║
+║                       │ bbbbbb     │                                  │                                                           ║
+╟───────────────────────┼────────────┼──────────────────────────────────┼───────────────────────────────────────────────────────────╢
+║ aaaaaa aaaaaa aaaaaa  │ bbbbbb     │ some long words somelongwords    │ dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd   ║
+║ aaaaaa aaaaaa aaaaaa  │ bbbbbb     │ somelongerwords                  │ dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd   ║
+║ aaaaaa aaaaaa aaaaaa  │ bbbbbb     │ someevenlongerwords              │ dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd   ║
+║ aaaaaa                │ bbbbbb     │ someevenmorelongerwords          │ dddddd                                                    ║
+║                       │ bbbbbb     │                                  │                                                           ║
+╟───────────────────────┼────────────┼──────────────────────────────────┼───────────────────────────────────────────────────────────╢
+║ aaaaaa aaaaaa aaaaaa  │ bbbbbb     │ cccccc cccccc cccccc cccccc      │ dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd   ║
+║ aaaaaa aaaaaa aaaaaa  │ bbbbbb     │ cccccc cccccc cccccc cccccc      │ dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd   ║
+║ aaaaaa aaaaaa aaaaaa  │ bbbbbb     │ cccccc cccccc cccccc cccccc      │ dddddd dddddd dddddd dddddd dddddd dddddd dddddd dddddd   ║
+║ aaaaaa                │ bbbbbb     │ cccccc cccccc cccccc             │ dddddd                                                    ║
+║                       │ bbbbbb     │                                  │                                                           ║
+╚═══════════════════════╧════════════╧══════════════════════════════════╧═══════════════════════════════════════════════════════════╝