If you'd like to print this:
┌───────────────────┐
│One Two Three Four│
╞═══════════════════╡
│1 2 3 4│
│5 6 7 8│
│9 [10] 11 12 │
└───────────────────┘
using this:
System.out.println(
new Table()
.addLine("One", "Two", "Three", "Four")
.addLine("1", "2", "3", "4")
.addLine("5", "6", "7", "8")
.addLine("9", "[10]", "11", "12")
// .--{ Optional configuration examples }--. //
.separateColumnsWith(" ")
.padWith(' ')
.boxWith(
"┌─┐" +
"╞═╡" +
"│ │" +
"└─┘"
)
.outputWith(System.out)
.alignRight()
.alignLeft(0)
.alignRight(1)
.alignCenter(2)
.alignLeft(3,3)
.boxHeader()
// '--{ Optional configuration examples }--' //
.toString()
);
all you need is this:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Collections;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.io.PrintStream;
public class Table {
private final List<Column> columns = new ArrayList<>();
// Defaults
private String columnSeparator = " ";
private String pad = " ";
private PrintStream out = System.out;
private Table.Alignment alignment;
private static final Table.Alignment LEFT = (left, word, right) -> word + left + right;
private static final Table.Alignment CENTER = (left, word, right) -> left + word + right;
private static final Table.Alignment RIGHT = (left, word, right) -> left + right + word;
private String boxCharacters;
private BoxType boarder;
{
alignLeft(); // Set default alignment
boxWith( // Set default boxing characters
".-." +
"|=|" +
"| |" +
"'-'"
);
boxOff(); // Set default boxing boarder
}
public Table addLine(String... line) {
if ( columns.size() > 0 && columns.size() != line.length) {
throw new IllegalArgumentException("Inconsistant arg. count.");
}
if ( columns.size() == 0 ) {
for (int i = 0; i < line.length; i++) {
columns.add( new Table.Column(alignment) );
}
}
IntStream
.range( 0, columns.size() )
.forEach( col -> columns.get(col).addWord(line[col]) )
;
return this;
}
// .---{ Boxing }---. //
public Table box() {
boarder = BoxType.BOX;
return this;
}
public Table boxOff() {
boarder = BoxType.BOXOFF;
return this;
}
public Table boxHeader() {
boarder = BoxType.HEAD;
return this;
}
// '---{ Boxing }---' //
public void print(){
out.println( toString() );
}
public String toString(){
final int rows = columns.get(0).size();
return new Table.Bound().box(
boarder,
IntStream
.range(0, rows)
.mapToObj(row -> columns
.stream()
.map( col -> col.getCell(row) )
.collect( Collectors.joining( columnSeparator ) )
)
.collect( Collectors.joining( System.lineSeparator() ) )
);
}
// .--{ Make defaults overridable }--. //
public Table outputWith(PrintStream out){
this.out = out;
return this;
}
public Table separateColumnsWith(String columnSeparator){
this.columnSeparator = columnSeparator;
return this;
}
public Table padWith(char pad){
this.pad = "" + pad;
return this;
}
public Table boxWith(String box){
this.boxCharacters = box;
return this;
}
// '--{ Make defaults overridable }--' //
public Table alignLeft(){
alignment = LEFT;
columns.forEach(col -> col.setAlignment(alignment));
return this;
}
public Table alignCenter(){
alignment = CENTER;
columns.forEach(col -> col.setAlignment(alignment));
return this;
}
public Table alignRight(){
alignment = RIGHT;
columns.forEach(col -> col.setAlignment(alignment));
return this;
}
public Table alignLeft(int col){
columns.get(col).setAlignment( LEFT );
return this;
}
public Table alignCenter(int col){
columns.get(col).setAlignment( CENTER );
return this;
}
public Table alignRight(int col){
columns.get(col).setAlignment( RIGHT );
return this;
}
public Table alignLeft(int col, int cell){
columns.get(col).setAlignment( LEFT, cell );
return this;
}
public Table alignCenter(int col, int cell){
columns.get(col).setAlignment( CENTER, cell );
return this;
}
public Table alignRight(int col, int cell){
columns.get(col).setAlignment( RIGHT, cell );
return this;
}
@FunctionalInterface
interface Alignment {
String align(String left, String word, String right);
}
enum BoxType {
BOXOFF(Table.Bound::boxOff),
BOX(Table.Bound::box),
HEAD(Table.Bound::boxHeader);
BiFunction<Table.Bound, String, String> boxLogic;
BoxType(BiFunction<Table.Bound, String, String> boxLogic){
this.boxLogic = boxLogic;
}
}
class Bound {
private String
cornerTL,
top,
cornerTR,
sideLH,
middleH,
sideRH,
sideL,
middle,
sideR,
cornerBL,
bottom,
cornerBR
;
{
setBoxCharacters( boxCharacters );
}
public void setBoxCharacters(String boxCharacters) {
if (boxCharacters.length() != 12) {
throw new IllegalArgumentException("box characters string unsupported size");
}
cornerTL = boxCharacters.substring(0,1);
top = boxCharacters.substring(1,2);
cornerTR = boxCharacters.substring(2,3);
sideLH = boxCharacters.substring(3,4);
middleH = boxCharacters.substring(4,5);
sideRH = boxCharacters.substring(5,6);
sideL = boxCharacters.substring(6,7);
middle = boxCharacters.substring(7,8);
sideR = boxCharacters.substring(8,9);
cornerBL = boxCharacters.substring(9,10);
bottom = boxCharacters.substring(10,11);
cornerBR = boxCharacters.substring(11,12);
}
public String box(BoxType boxType, String body){
return boxType.boxLogic.apply(this, body);
}
public String boxOff(String body){
return body;
}
public String box(String body){
final int width = body.indexOf(System.lineSeparator());
// One Two Three Four
// 1 2 3 4
// 5 6 7 8
body = body.replace(System.lineSeparator(), sideR + System.lineSeparator() + sideL);
// One Two Three Four|
// |1 2 3 4 |
// |5 6 7 8
body =
cornerTL + top.repeat(width) + cornerTR + System.lineSeparator() +
sideL + body + sideR + System.lineSeparator() +
cornerBL + bottom.repeat(width) + cornerBR
;
// .------------------.
// |One Two Three Four|
// |1 2 3 4 |
// |5 6 7 8 |
// '------------------'
return body;
}
public String boxHeader(String body){
final int width = body.indexOf(System.lineSeparator());
// One Two Three Four
// 1 2 3 4
// 5 6 7 8
body = body.replaceFirst(
System.lineSeparator(),
System.lineSeparator() + middleH.repeat(width) + System.lineSeparator()
);
// One Two Three Four
// ------------------
// 1 2 3 4
// 5 6 7 8
body = box(body);
// .------------------.
// |One Two Three Four|
// |------------------|
// |1 2 3 4 |
// |5 6 7 8 |
// '------------------'
//Replace header sides
final int ls = System.lineSeparator().length();
final int lineLength = 1+width+1+ls;
body =
body.substring( 0, 2*lineLength ) +
sideLH + middleH.repeat(width) + sideRH + System.lineSeparator() +
body.substring( 3*lineLength, body.length() )
;
// .------------------.
// |One Two Three Four|
// }------------------{
// |1 2 3 4 |
// |5 6 7 8 |
// '------------------'
return body;
}
}
class Column {
private List<Cell> words = new ArrayList<>();
private int maxLength = 0;
private Table.Alignment alignment;
public Column(Table.Alignment alignment){
setAlignment(alignment);
}
public Table.Column addWord(String word){
maxLength = Math.max(maxLength, word.length());
words.add( new Cell(word) );
return this;
}
public String getCell(int row){
return words.get(row).padCell( maxLength );
}
public int size(){
return words.size();
}
public String toString(){
return words.toString();
}
public Column setAlignment(Table.Alignment alignment){
this.alignment = alignment;
words.forEach(cell -> cell.setAlignment(alignment));
return this;
}
public Column setAlignment(Table.Alignment alignment, int cell){
words.get(cell).setAlignment(alignment);
return this;
}
class Cell {
private String word;
private Table.Alignment alignment;
public Cell(String word){
this.word = word;
}
public void setAlignment(Table.Alignment alignment){
this.alignment = alignment;
}
public String toString(){
return padCell(maxLength);
}
private String padCell(int newLength){
int padCount = newLength - word.length();
int leftCount = padCount / 2;
int rightCount = padCount - leftCount;
String left = pad.repeat(leftCount);
String right = pad.repeat(rightCount);
return alignment.align(left, word, right);
}
}
}
}
By waiting until all the lines have been added before printing it can figure out the width each column needs.
Looking for a code review to reveal problems. Anything from better names to fewer bugs to better ideas.
Since my 4th attempt reviews have inspired the following changes:
- Regarding the review by @AJNeufeld
- Removed
padWith(String)
in favor of only providingpadWith(char)
- Put it in a box see
.box()
and.boxWith(String)
- Made
.print()
a terminating call by making it's return typevoid
- Removed
- Regarding the review by @Roman
- Changed name of top class to
Table
- Changed name of top class to
- Regarding the review by @RoToRa
- Added
Cell
class that allows alignment to be controlled on each string - Added
Alignment
functional interface and removed generic curried Alignment function - Rewrote
toString()
to useCollector
and removed use ofStringBuilder
- Replaced
getAlign*
methods with constants
- Added
Also added BoxType
enum for the different boxing method references by following this.
My thanks to w3.org for providing box drawing Unicode characters I could copy and paste.