0
\$\begingroup\$

If you'd like to print this:

One Two Three Four 
1     2   3      4 

using this:

new Columns()
    .addLine("One", "Two", "Three", "Four")
    .addLine("1", "2", "3", "4")

    // .-={ Optional configuration examples }=-. //
    .separateColumnsWith(" ")
    .padWith(" ")
    .padWith(' ')
    .outputWith(System.out)
    .alignRight()
    .alignLeft(0)        
    .alignRight(1)
    .alignCenter(2)
    // '-={ Optional configuration examples }=-' //            

    .print()
;

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.Function;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.io.PrintStream;

public class Columns {

    private final List<Column> columns = new ArrayList<>();

    // Defaults
    private String columnSeparator = " ";
    private String pad = " ";
    private PrintStream out = System.out;
    private Function<String, Function<String, UnaryOperator<String>>> align;

    {
        alignLeft(); // Set default alignment
    }

    public Columns 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 Columns.Column(align) );
            }
        }
        
        IntStream
            .range(0, columns.size())
            .forEach( col -> columns.get(col).addWord(line[col]) )
        ;

        return this;
    }
    
    public Columns print(){
        out.println( toString() );
        return this;
    }

    public String toString(){
        
        final StringBuilder result = new StringBuilder();
        final int rows = columns.get(0).size();

        IntStream
            .range(0, rows)
            .forEach( 
                row -> {
                    result
                        .append( 
                            columns
                            .stream()
                            .map(col -> col.getCell(row))
                            .collect( Collectors.joining(columnSeparator) )
                        )
                    ;
                    result.append( System.lineSeparator() );
                }
            )
        ;

        return result.toString();
    }

   // .-={ Make defaults overridable }=-. //
   public Columns outputWith(PrintStream out){
        this.out = out;
        return this;
    }

    public Columns separateColumnsWith(String columnSeparator){
        this.columnSeparator = columnSeparator;
        return this;
    }
        
    public Columns padWith(String pad){
        if (pad.length() != 1) {
            throw new IllegalArgumentException("Expected single character");
        }
        this.pad = pad;
        return this;
    }
    
    public Columns padWith(char pad){
        this.pad = "" + pad;
        return this;
    }
    // '-={ Make defaults overridable }=-' //

    public Columns alignLeft(){
        align = getAlignLeft();
        columns.forEach(col -> col.setAlign(align));
        return this;
    }

    public Columns alignCenter(){
        align = getAlignCenter();
        columns.forEach(col -> col.setAlign(align));
        return this;
    }

    public Columns alignRight(){
        align = getAlignRight();
        columns.forEach(col -> col.setAlign(align));
        return this;
    }

    public Columns alignLeft(int col){
        columns.get(col).setAlign( getAlignLeft() );
        return this;
    }

    public Columns alignCenter(int col){
        columns.get(col).setAlign( getAlignCenter() );
        return this;
    }

    public Columns alignRight(int col){
        columns.get(col).setAlign( getAlignRight() );
        return this;
    }

    private Function<String, Function<String, UnaryOperator<String>>> getAlignLeft(){
        return left -> word -> right -> word + left + right;
    }

    private Function<String, Function<String, UnaryOperator<String>>> getAlignCenter(){
        return left -> word -> right -> left + word + right;
    }

    private Function<String, Function<String, UnaryOperator<String>>> getAlignRight(){
        return left -> word -> right -> left + right + word;
    }

    class Column {
        private List<String> words = new ArrayList<>();
        private int maxLength = 0;
        private Function<String, Function<String, UnaryOperator<String>>> align;

        public Column(Function<String, Function<String, UnaryOperator<String>>> align){
            this.align = align;
        }
        
        public Columns.Column addWord(String word){
            maxLength = Math.max(maxLength, word.length());
            words.add(word);
            return this;
        }
        
        public String getCell(int row){
            return padCell( words.get(row), maxLength );
        }
        
        private String padCell(String word, 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 align.apply(left).apply(word).apply(right);
        }
        
        public int size(){
            return words.size();
        }
        
        public String toString(){
            return words.toString();
        }
        
        public Column setAlign(Function<String, Function<String, UnaryOperator<String>>> align){
            this.align = align;
            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.

Back on my second attempt @torbenputkonen gave me the idea of using an inner class for each column. As predicted it cleaned up the code and gave the flexibility to manage alignment on each column.


The reviews here have inspired a 5th attempt.

\$\endgroup\$
3
  • \$\begingroup\$ Happy to see another Orange 🍊 \$\endgroup\$ Mar 25 at 5:13
  • \$\begingroup\$ @eccentricOrange likewise. There do seem to be a fair number of us. Any thoughts on the code here? \$\endgroup\$ Mar 25 at 13:58
  • \$\begingroup\$ Ah sorry, nope! I don't know Java sufficiently well \$\endgroup\$ Mar 25 at 16:29

3 Answers 3

1
\$\begingroup\$

I’m not wild about the padWith(String), and restricting the string to a single character. Just keep the padWith(char) method.

You can separate columns with " | ", but you can’t specify a prefix or suffix, so you can’t make vertical lines at the left and right edges.

While we’re at it, it might be nice to have top/bottom lines possible, and perhaps a line after headings. But if you go too far down that rabbit hole, you’d probably want to call the class Table.

.print().addLine(…).print() has unexpected behaviour. I don’t see the point of allowing anything to chain after doing a print unless it clears the accumulated buffer. Perhaps it should be void.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you for the review. You'll find some of your ideas implemented in my 5th attempt. \$\endgroup\$ Apr 2 at 16:57
1
+50
\$\begingroup\$

This is getting interesting.


The Column class is a good idea. Maybe it's worth considering going even a step further and using a grid of "cells", allowing different alignments per row, too.


I'm still not a fan of tying this to a PrintStream. Specifically I don't see the point of the out field. I'd just pass the PrintStream as a parameter to the print() method.


And the curried functional interface is still quite clunky. It doesn't need to be a generic TriFunction<A, B, C, R> like you said last time, but it should be a specific AlignFunction.


While I normally like the use of Streams, using an IntStream just with forEach, is IMO a bit pointless. Unfortunatly I can't think of a better way due to Java's Stream API limits. At the least I'd replace the StringBuilder in toString with another joining Collector.


The getAlign* methods should be replaced by constants.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Thanks for the review. After 10 days I started to worry no one could read it. By AlignFunction I presume you mean something like: @FunctionalInterface interface Alignment { String align(String left, String word, String right); } \$\endgroup\$ Mar 30 at 16:56
  • \$\begingroup\$ As you suggested, I managed to drop StringBuilder and use another joining Collector See here. \$\endgroup\$ Mar 31 at 5:13
  • \$\begingroup\$ As for the out field, I'm trying to decouple code that knows where to send this string from code that knows when. Can't do that if out is passed to the print() method. I'd prefer to keep configuration code separate from behavior code. \$\endgroup\$ Mar 31 at 5:24
  • \$\begingroup\$ Thank you for the review. You'll find some of your ideas implemented in my 5th attempt. \$\endgroup\$ Apr 2 at 16:57
1
\$\begingroup\$

It is possible that I overthought your current solution. But after a while it confused me that the name of the class is Columns but we are adding lines/rows to it. If the class name were Table everything would be right. But because its name is Columns I await that we are adding columns to it, like in the following API:

new Columns()
    .add("One", "1")
    .add("Two", "2")
    .add("Three", "3")
    .add("Four", "4")

    // .-={ Optional configuration examples }=-. //
    .separateColumnsWith(" ")
    .padWith(" ")
    .padWith(' ')
    .outputWith(System.out)
    .alignRight()
    .alignLeft(0)        
    .alignRight(1)
    .alignCenter(2)
    // '-={ Optional configuration examples }=-' // 

    .print()

This change on the API would simplify the method addLine (which I renamed to add):

public Columns add(String... values) {
    if (columns.size() > 0 && columns.size() != values.length) {
        throw new IllegalArgumentException("Inconsistant arg. count.");
    }
    
    Column column = new Columns.Column(align);

    for (int i = 0; i < values.length; i++) {
        column.addWord(values[i]);
    }

    columns.add(column);    

    return this;
}

Because of the change of the API, I asked my self, if the methods alignCenter, alignRight and alignLeft still belong to Columns. Maybe they fit better into "Column". This would reduce the amount of methods a person needs to know. It is for me personally more intuitive too:

new Columns()
    .add(Column.alignedLeft("One", "1"))
    .add(Column.alignedRight("Two", "2"))
    .add(Column.alignedCentered("Three", "3"))
    .add(Column.alignedRight("Four", "4"))

    // .-={ Optional configuration examples }=-. //
    .separateColumnsWith(" ")
    .padWith(" ")
    .padWith(' ')
    .outputWith(System.out)
    // '-={ Optional configuration examples }=-' // 

    .print()
\$\endgroup\$
3
  • \$\begingroup\$ You have a point about the name. Just not seeing as much need for the pivoted interface. Any other thoughts? \$\endgroup\$ Apr 1 at 19:44
  • \$\begingroup\$ Thank you for the review. You'll find some of your ideas implemented in my 5th attempt. \$\endgroup\$ Apr 2 at 16:57
  • \$\begingroup\$ Couldn't stop thinking about your interface. You'll now find Table has a toPivot() method in my 6th attempt. \$\endgroup\$ Apr 6 at 19:47

Your Answer

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

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