If you'd like to print this:
One Two Three Four
1 2 3 4
using this:
new Columns()
.withColumnSeparator(" ")
.withPad(" ")
// .alignLeft()
.alignCenter()
// .alignRight()
.addLine("One", "Two", "Three", "Four")
.addLine("1", "2", "3", "4")
.print()
;
all you need is this:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.UnaryOperator;
public class Columns {
private List<List<String>> lines = new ArrayList<>();
private List<Integer> maxLengths = new ArrayList<>();
private String columnSeparator = " ";
private String pad = " ";
private Function<String, Function<String, UnaryOperator<String>>> align;
{
alignLeft(); // set default alignment
}
public Columns addLine(String... line) {
if (maxLengths.size() == 0){
for(int column = 0; column < line.length; column++) {
maxLengths.add(0);
}
}
if ( maxLengths.size() != line.length ) {
throw new IllegalArgumentException();
}
for(int column = 0; column < line.length; column++) {
int length = Math
.max(
maxLengths.get(column),
line[column].length()
)
;
maxLengths.set( column, length );
}
lines.add( Arrays.asList(line) );
return this;
}
public Columns print(){
System.out.println( toString() );
return this;
}
public String toString(){
int totalLength = 0;
for(List<String> line : lines) {
for(int column = 0; column < line.size(); column++) {
totalLength += maxLengths.get(column);
if (column < line.size() - 1) {
totalLength += columnSeparator.length();
}
}
totalLength += System.lineSeparator().length();
}
StringBuilder result = new StringBuilder(totalLength);
for(List<String> line : lines) {
for(int column = 0; column < line.size(); column++) {
result.append(
padCell(
line.get(column),
maxLengths.get(column)
)
);
if (column < line.size() - 1) {
result.append(columnSeparator);
}
}
result.append( System.lineSeparator() );
}
return result.toString();
}
private String padCell(String word, int newLength){
int padCount = newLength - word.length();
int leftCount = padCount / 2;
int rightCount = padCount - leftCount;
String left = new String(new char[leftCount]).replace("0円", pad);
String right = new String(new char[rightCount]).replace("0円", pad);
return align.apply(left).apply(word).apply(right);
}
public Columns separateColumnsWith(String columnSeparator){
this.columnSeparator = columnSeparator;
return this;
}
public Columns padWith(String pad){
this.pad = pad;
return this;
}
public Columns alignLeft(){
align = left -> word -> right -> word + left + right;
return this;
}
public Columns alignCenter(){
align = left -> word -> right -> left + word + right;
return this;
}
public Columns alignRight(){
align = left -> word -> right -> left + right + word;
return this;
}
}
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.
Along with some minor fixes the string concatenation has been modified which simplified the alignment lambdas (and lengthened padCell() and toString()). Micro-optimization?
1 Answer 1
Those are some interesting and inventive changes. Here are some thoughs (some new, some of which I missed - or just left out - last time):
First off, a bug: You are assuming that pad has a length of 1. I'd change it to a char or validate it in the setter.
I like the initialisation of align instead of the duplicate lambda.
The initializer block is an obscure feature and I believe, if there were multiple contructors its code is copied into each of them by the compiler creating artifical code duplicate. That's why I'd personally put it into a constructor, but I guess in this case the initializer block is fine, too.
Using a "curried" (is that the right word?) type to create the type of align is interesting. I guess it's fine here, but if the type would be needed at multiple locations this could become cumbersome. Personally I'd define my own AlignFunction functional interface.
In addLine() it may be sensibe to disallow lines with zero items. I can't see any immediate problems for that edge case, but it would avoid potential future problems.
An else between the two if would be good, because the two cases are exclusive.
The filling of maxLength could be done with Collections.nCopies().
Actually thinking about it, I'd drop the maxLength field and just calculate the maximum lengths in the toString method.
I'd drop the print() method to avoid the unnecessary coupling with the console.
The calculation of the size of the StringBuffer is unnecessary IMO, but if you do it, you don't need to loop of the lines. It can be done with just summing up maxLengths and multiplication:
// Or just a loop. I prefer `Stream`s.
final int totalMaxLengths = maxLength.stream().reduce(0, (a, b) -> a + b);
// TODO Format better
final int totalLength = (totalMaxLengths + columnSeparator.length() * (maxLengths.size() - 1) + System.lineSeparator().length()) * lines.size();
For the actual output building I suggested StringBuilder last time, but I'd probably also use Streams.
The repeatition of strings in padCell is also interesting (you got it from Stackoverflow didn't you :) ). Since Java 11 however there is String::repeat(). Or at the very least extract the expression into a well named method.
-
\$\begingroup\$ Sᴀᴍ Onᴇᴌᴀ ♦ seems to be concerned I invalidated your answer by fixing the construction code. But I didn't see you mention
separateColumnsWith()orpadWith(). Since I'm not allowed to fix it, what do you think of the new names? I know you had concerns with the old ones. \$\endgroup\$candied_orange– candied_orange2023年03月14日 17:04:52 +00:00Commented Mar 14, 2023 at 17:04 -
\$\begingroup\$ Ah, I had forgotten to mention that: the names without the prefixes are better. The "With" suffix is a good idea, too. \$\endgroup\$RoToRa– RoToRa2023年03月15日 10:10:20 +00:00Commented Mar 15, 2023 at 10:10
-
\$\begingroup\$ You've inspired a third attempt! And yes currying is when you break down a multi-arg function into a chain of partial single arg functions. Looks weird but it avoids the need to create a
TriFunctioninterface. \$\endgroup\$candied_orange– candied_orange2023年03月16日 15:46:01 +00:00Commented Mar 16, 2023 at 15:46
/**comments would be welcome. LGTM, ship it! \$\endgroup\$