4
\$\begingroup\$

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?

\$\endgroup\$
6
  • \$\begingroup\$ Nice! I really like the Builder pattern. The use of whitespace is a bit idiosyncratic, but I'm sure Jalopy or similar could tidy it up when incorporating this into another codebase. More /** comments would be welcome. LGTM, ship it! \$\endgroup\$
    – J_H
    Commented Mar 14, 2023 at 7:17
  • \$\begingroup\$ @J_H Or you could tidy up the codebase. Properly showing structure with white space rather than surrendering to word wrapped noise may be idiosyncratic but that doesn't make it bad. I guess some will always prefer blindly using tools instead of good judgement. \$\endgroup\$ Commented Mar 14, 2023 at 10:29
  • \$\begingroup\$ @J_H failing that, well, //@formatting:off, exists for a reason. \$\endgroup\$ Commented Mar 14, 2023 at 10:42
  • \$\begingroup\$ I have rolled back Rev 3 → 2. Please see What to do when someone answers. \$\endgroup\$ Commented Mar 14, 2023 at 16:09
  • \$\begingroup\$ Did you consider creating an internal class for a single Column? I believe it would clean up the code considerably and also make adding functionality, like column-specific alignment, easier. \$\endgroup\$ Commented Mar 16, 2023 at 6:42

1 Answer 1

4
\$\begingroup\$

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.

\$\endgroup\$
3
  • \$\begingroup\$ Sᴀᴍ Onᴇᴌᴀ ♦ seems to be concerned I invalidated your answer by fixing the construction code. But I didn't see you mention separateColumnsWith() or padWith(). 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\$ Commented 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
    Commented 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 TriFunction interface. \$\endgroup\$ Commented Mar 16, 2023 at 15:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.