I wrote a class which can format input so that it is properly aligned.
I'm interested in all suggestions (regarding structure, usability, style, naming, comments, etc).
I'm especially wondering about:
- Is there a better way to write the tests? I want to first test the
Content
class, and then test theAlign
class (so that when an error is thrown, I can see right away where it is). Right now, I'm basically just copy-pasting theContent
tests (theadd
andset
tests), and addingalign.output((String s) -> nothing());
. This doesn't seem like the right way to do it.
Example Usage:
public static void main(String[] args) {
Align align = new Align();
// header
align.addLine("Name", "Age", "Address", "Phone");
// data
align.addLine("Alice", "32", "United States", "555-123456");
align.addLine("Bob", "12", "nowhere", "555-123456");
align.addLine("Carol", "78", "1080 Maple Court Cape Girardeau, MO 63701", "555-123456");
align.addLine("Ed", "159", "Fake Stree, Fakesville");
// output
align.output((String s) -> System.out.println(s));
// add a forgotten column
align.addColumn(3, "Hair Color", "Red", "Brown", "Blue", "Purple", "Orange");
align.output((String s) -> System.out.println(s));
}
Output:
Name Age Address Phone
Alice 32 United States 555-123456
Bob 12 nowhere 555-123456
Carol 78 1080 Maple Court Cape Girardeau, MO 63701 555-123456
Ed 159 Fake Stree, Fakesville -
Name Age Address Hair Color Phone
Alice 32 United States Red 555-123456
Bob 12 nowhere Brown 555-123456
Carol 78 1080 Maple Court Cape Girardeau, MO 63701 Blue 555-123456
Ed 159 Fake Stree, Fakesville Purple -
- - - Orange -
Align Class:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
public class Align {
private String paddingChar = "-";
private int minSpacing = 5;
protected Content content;
public Align() {
content = new Content();
}
/**
* set the character that is used for values that don't exist.
*
* @param paddingChar padding char
*/
public void setPaddingChar(String paddingChar) {
this.paddingChar = paddingChar;
}
/**
* the minimum amount of spaces between columns in the formated output.
*
* Must be at least 0, but a minimum of at least 1 is suggested.
*
* @param minSpacing minimum of spaces (must be at least 0)
*/
public void setMinSpacing(int minSpacing) {
if (minSpacing < 0) {
throw new IllegalArgumentException("min spacing must be at least 0");
}
this.minSpacing = minSpacing;
}
public void addLine(int position, List<String> lineContent) {
content.addLine(position, lineContent);
}
public void addLine(List<String> lineContent) {
addLine(content.lineCount(), lineContent);
}
public void addLine(String... lineContent) {
addLine(content.lineCount(), new ArrayList<>(Arrays.asList(lineContent)));
}
public void addLine(int position, String... lineContent) {
addLine(position, new ArrayList<>(Arrays.asList(lineContent)));
}
public void addColumn(int position, List<String> columnContent) {
content.addColumn(position, columnContent);
}
public void addColumn(List<String> columnContent) {
addColumn(content.columnCount(), columnContent);
}
public void addColumn(String... columnContent) {
addColumn(content.columnCount(), new ArrayList<>(Arrays.asList(columnContent)));
}
public void addColumn(int position, String... columnContent) {
addColumn(position, new ArrayList<>(Arrays.asList(columnContent)));
}
/**
* sets the value at the given column and row to the given content.
*
* If the value doesn't exist already, an IllegalArgumentException will be
* thrown.
*
* @param column column
* @param row row
* @param value value
*/
public void set(int column, int row, String value) {
content.set(column, row, value);
}
/**
* passes the formated string to consumer.
*
* To get the formated string, use toString.
*
* @param consumer consumer
*/
public void output(Consumer<String> consumer) {
consumer.accept(toString());
}
@Override
public String toString() {
List<Integer> maxContentLength = getMaxLengths();
List<String> spaces = getSpaces(maxContentLength);
StringBuilder out = new StringBuilder();
for (Content.Line line : content.lines) {
for (int columnIndex = 0; columnIndex < line.size(); columnIndex++) {
String column = line.get(columnIndex);
out.append(column);
out.append(spaces.get(columnIndex)
.substring(0,
spaces.get(columnIndex).length()
- column.length())); // align
}
out.append("\n");
}
return out.toString();
}
/**
* returns a list of spaces of the given lengths.
*
* @param lengths lengths
* @return list of spaces
*/
private List<String> getSpaces(List<Integer> lengths) {
List<String> spaces = new ArrayList<>();
for (Integer length : lengths) {
spaces.add(getSpaces(length));
}
return spaces;
}
/**
* returns n spaces.
*
* @param n number of spaces
* @return n spaces
*/
private static String getSpaces(int n) {
StringBuilder spaces = new StringBuilder(n);
for (int i = 0; i < n; i++) {
spaces.append(" ");
}
return spaces.toString();
}
/**
* returns a list containing the length of the longest string of each
* column.
*
* @return lengths
*/
private List<Integer> getMaxLengths() {
List<Integer> maxContentLength = new ArrayList<>();
for (int i = 0; i <= content.columnCount(); i++) {
maxContentLength.add(0); // init max length with 0
}
for (Content.Line line : content.lines) {
for (int columnIndex = 0; columnIndex < content.columnCount(); columnIndex++) {
String column = line.get(columnIndex);
if (column.length() > maxContentLength.get(columnIndex)) {
maxContentLength.set(columnIndex, column.length() + minSpacing);
}
}
}
return maxContentLength;
}
protected class Content {
private List<Line> lines;
public Content() {
lines = new ArrayList<>();
}
/**
* returns number of lines.
*
* @return number of lines
*/
public int lineCount() {
return lines.size();
}
/**
* returns number of columns.
*
* @return number of columns
*/
public int columnCount() {
if (lines.isEmpty()) {
return 0;
}
return lines.get(0).size(); // all lines are the same length
}
public void set(int column, int row, String value) {
if (row < 0 || row > lineCount()) {
throw new IllegalArgumentException("row doesn't exist");
}
Line line = lines.get(row);
if (column < 0 || column > columnCount()) {
throw new IllegalArgumentException("column doesn't exist");
}
line.set(column, value);
}
public void addColumn(int position, List<String> columnContent) {
if (position < 0) {
throw new IllegalArgumentException("Column position is negative");
}
// if the new position is outside the current content, extend short lines to position
if (position > columnCount()) {
for (Line line : lines) {
padTo(line, position);
}
}
// add new lines for content of this column in case the column is too long
while (lines.size() < columnContent.size()) {
Line newLine = new Line(new ArrayList<>());
padTo(newLine, position); // if the current line is new, and thus too short, pad it
lines.add(newLine);
}
for (int i = 0; i < columnContent.size(); i++) {
Line line = lines.get(i);
line.add(position, columnContent.get(i)); // add actual content of column
}
if (columnContent.size() > columnCount()) {
for (Line line : lines) {
padTo(line, content.columnCount());
}
}
// if the column was shorter then previous once, shift the rest of the lines and insert padding char for the new column
if (columnContent.size() < lines.size()) {
shift(position, lines.subList(columnContent.size(), lines.size()));
}
}
public void addLine(int position, List<String> lineContent) {
if (position < 0) {
throw new IllegalArgumentException("Line position is negative");
}
// add elements to this line if it is too short
padTo(lineContent, columnCount());
if (lineContent.size() > columnCount()) { // add elements to all other lines if this line is the new longest line
for (Line line : lines) {
padTo(line, lineContent.size()); // padd to new longest line
}
}
while (lines.size() < position) { // add new empty lines if this line is outside field
List<String> emptyLineContent = new ArrayList<>();
for (int i = 0; i < columnCount(); i++) {
emptyLineContent.add(paddingChar);
}
lines.add(new Line(emptyLineContent));
}
lines.add(position, new Line(lineContent)); // add actual line
}
/**
* shifts all given lines at given position one entry to the right.
*
* The padding character will be inserted at the new position.
*
* @param position position
* @param list list
*/
private void shift(int position, List<Line> list) {
for (Line line : list) {
line.add(position, paddingChar);
}
}
/**
* add elements to given line until its length is the given max.
*
* @param line line
* @param max max
*/
private void padTo(List<String> line, int max) {
while (line.size() < max) {
line.add(paddingChar);
}
}
private class Line extends ArrayList<String> {
public Line(List<String> line) {
super(line);
}
}
}
}
Tests:
import org.junit.Test;
public class AlignTest {
private static Align getAlign(int columns, int rows) {
Align align = new Align();
for (int i = 0; i < rows; i++) {
String[] row = new String[columns];
for (int j = 0; j < columns; j++) {
row[j] = String.valueOf(i) + String.valueOf(j);
}
align.addLine(row);
}
return align;
}
/** TEST CONTENT **/
@Test(expected=IllegalArgumentException.class)
public void setNegativeColumn() {
getAlign(2, 3).set(-1, 1, "new value");
}
@Test(expected=IllegalArgumentException.class)
public void setNonExistingColumn() {
getAlign(2, 3).set(20, 1, "new value");
}
@Test(expected=IllegalArgumentException.class)
public void setNegativeLine() {
getAlign(2, 3).set(1, -1, "new value");
}
@Test(expected=IllegalArgumentException.class)
public void setNonExistingLine() {
getAlign(2, 3).set(1, 20, "new value");
}
@Test()
public void setExisting() {
getAlign(2, 3).set(1, 1, "new value");
}
@Test()
public void setExistingCornerCases() {
getAlign(3, 3).set(0, 1, "new value");
getAlign(3, 3).set(1, 0, "new value");
getAlign(3, 3).set(1, 2, "new value");
getAlign(3, 3).set(2, 1, "new value");
}
@Test(expected=IllegalArgumentException.class)
public void addNegativeColumn() {
getAlign(2, 3).addColumn(-1, "new");
}
@Test(expected=IllegalArgumentException.class)
public void addNegativeLine() {
getAlign(2, 3).addLine(-1, "new");
}
@Test()
public void addShortColumn() {
getAlign(2, 3).addColumn(1, "new");
}
@Test()
public void addShortColumnOutsideField() {
getAlign(2, 3).addColumn(5, "new");
}
@Test()
public void addLongColumn() {
getAlign(2, 3).addColumn(1, "new", "new", "new", "new", "new");
}
@Test()
public void addLongColumnOutsideField() {
getAlign(2, 3).addColumn(5, "new", "new", "new", "new", "new");
}
@Test()
public void addShortLine() {
getAlign(2, 3).addLine(1, "new");
}
@Test()
public void addShortLineOutsideField() {
getAlign(2, 3).addLine(5, "new");
}
@Test()
public void addLongLine() {
getAlign(2, 3).addLine(1, "new", "new", "new", "new", "new");
}
@Test()
public void addLongLineOutsideField() {
getAlign(2, 3).addLine(5, "new", "new", "new", "new", "new");
}
/** TEST ALIGN **/
@Test()
public void outputSetExisting() {
Align align = getAlign(2, 3);
align.set(1, 1, "new value");
align.output((String s) -> nothing(s));
}
@Test()
public void outputAddShortColumn() {
Align align = getAlign(2, 3);
align.addColumn(1, "new");
align.output((String s) -> nothing(s));
}
@Test()
public void outputAddShortColumnOutsideField() {
Align align = getAlign(2, 3);
align.addColumn(5, "new");
align.output((String s) -> nothing(s));
}
@Test()
public void outputAddLongColumn() {
Align align = getAlign(2, 3);
align.addColumn(1, "new", "new", "new", "new", "new");
align.output((String s) -> nothing(s));
}
@Test()
public void outputAddLongColumnOutsideField() {
Align align = getAlign(2, 3);
align.addColumn(5, "new", "new", "new", "new", "new");
align.output((String s) -> nothing(s));
}
@Test()
public void outputAddShortLine() {
Align align = getAlign(2, 3);
align.addLine(1, "new");
align.output((String s) -> nothing(s));
}
@Test()
public void outputAddShortLineOutsideField() {
Align align = getAlign(2, 3);
align.addLine(5, "new");
align.output((String s) -> nothing(s));
}
@Test()
public void outputAddLongLine() {
Align align = getAlign(2, 3);
align.addLine(1, "new", "new", "new", "new", "new");
align.output((String s) -> nothing(s));
}
@Test()
public void outputAddLongLineOutsideField() {
Align align = getAlign(2, 3);
align.addLine(5, "new", "new", "new", "new", "new");
align.output((String s) -> nothing(s));
}
public void nothing(String s) {
//System.out.println(s + "\n\n");
}
}