14

I'm looking for a util which will print a rectangular String[][] into a human-readable table with correct column lengths.

DD.
  • 21,498
  • 52
  • 157
  • 246

5 Answers5

25

If you want something similar to MySQL command-line client output, you can use something like that:

import java.io.PrintStream;

import static java.lang.String.format;
import static java.lang.System.out;

public final class PrettyPrinter {

    private static final char BORDER_KNOT = '+';
    private static final char HORIZONTAL_BORDER = '-';
    private static final char VERTICAL_BORDER = '|';

    private static final String DEFAULT_AS_NULL = "(NULL)";

    private final PrintStream out;
    private final String asNull;

    public PrettyPrinter(PrintStream out) {
        this(out, DEFAULT_AS_NULL);
    }

    public PrettyPrinter(PrintStream out, String asNull) {
        if ( out == null ) {
            throw new IllegalArgumentException("No print stream provided");
        }
        if ( asNull == null ) {
            throw new IllegalArgumentException("No NULL-value placeholder provided");
        }
        this.out = out;
        this.asNull = asNull;
    }

    public void print(String[][] table) {
        if ( table == null ) {
            throw new IllegalArgumentException("No tabular data provided");
        }
        if ( table.length == 0 ) {
            return;
        }
        final int[] widths = new int[getMaxColumns(table)];
        adjustColumnWidths(table, widths);
        printPreparedTable(table, widths, getHorizontalBorder(widths));
    }

    private void printPreparedTable(String[][] table, int widths[], String horizontalBorder) {
        final int lineLength = horizontalBorder.length();
        out.println(horizontalBorder);
        for ( final String[] row : table ) {
            if ( row != null ) {
                out.println(getRow(row, widths, lineLength));
                out.println(horizontalBorder);
            }
        }
    }

    private String getRow(String[] row, int[] widths, int lineLength) {
        final StringBuilder builder = new StringBuilder(lineLength).append(VERTICAL_BORDER);
        final int maxWidths = widths.length;
        for ( int i = 0; i < maxWidths; i++ ) {
            builder.append(padRight(getCellValue(safeGet(row, i, null)), widths[i])).append(VERTICAL_BORDER);
        }
        return builder.toString();
    }

    private String getHorizontalBorder(int[] widths) {
        final StringBuilder builder = new StringBuilder(256);
        builder.append(BORDER_KNOT);
        for ( final int w : widths ) {
            for ( int i = 0; i < w; i++ ) {
                builder.append(HORIZONTAL_BORDER);
            }
            builder.append(BORDER_KNOT);
        }
        return builder.toString();
    }

    private int getMaxColumns(String[][] rows) {
        int max = 0;
        for ( final String[] row : rows ) {
            if ( row != null && row.length > max ) {
                max = row.length;
            }
        }
        return max;
    }

    private void adjustColumnWidths(String[][] rows, int[] widths) {
        for ( final String[] row : rows ) {
            if ( row != null ) {
                for ( int c = 0; c < widths.length; c++ ) {
                    final String cv = getCellValue(safeGet(row, c, asNull));
                    final int l = cv.length();
                    if ( widths[c] < l ) {
                        widths[c] = l;
                    }
                }
            }
        }
    }

    private static String padRight(String s, int n) {
        return format("%1$-" + n + "s", s);
    }

    private static String safeGet(String[] array, int index, String defaultValue) {
        return index < array.length ? array[index] : defaultValue;
    }

    private String getCellValue(Object value) {
        return value == null ? asNull : value.toString();
    }

}

And use it like that:

final PrettyPrinter printer = new PrettyPrinter(out);
printer.print(new String[][] {
        new String[] {"FIRST NAME", "LAST NAME", "DATE OF BIRTH", "NOTES"},
        new String[] {"Joe", "Smith", "November 2, 1972"},
        null,
        new String[] {"John", "Doe", "April 29, 1970", "Big Brother"},
        new String[] {"Jack", null, null, "(yes, no last name)"},
});

The code above will produce the following output:

+----------+---------+----------------+-------------------+
|FIRST NAME|LAST NAME|DATE OF BIRTH   |NOTES              |
+----------+---------+----------------+-------------------+
|Joe       |Smith    |November 2, 1972|(NULL)             |
+----------+---------+----------------+-------------------+
|John      |Doe      |April 29, 1970  |Big Brother        |
+----------+---------+----------------+-------------------+
|Jack      |(NULL)   |(NULL)          |(yes, no last name)|
+----------+---------+----------------+-------------------+
Lyubomyr Shaydariv
  • 20,327
  • 12
  • 64
  • 105
  • 1
    @user230137 true, but this is how MySQL output works. If you make a `SELECT` query, in the native MySQL command line client, that results into rows with new-line characters, then the output is broken as well, because the output characters are not processed in any way. The only known to me difference is that my implementation is a little more compact, because it does not have 1 space character horizontal padding unlike MySQL command line output. – Lyubomyr Shaydariv Jul 09 '14 at 10:09
9

You can try

System.out.println(Arrays.deepToString(someRectangularStringArray));

And that's as pretty as it'll get without specific code.

fvu
  • 32,488
  • 6
  • 61
  • 79
  • 2
    @DD. I know it isn't but as the original question was a bit light on details I wanted to point out this as the (afaik) only no-code maybe good-enough solution.... – fvu Jul 08 '12 at 17:05
  • Good solution but to get this prettier I had to use a further `replace` on the result as advised [here](http://stackoverflow.com/questions/12845208#17423410). – Steve Chambers Aug 17 '16 at 12:50
  • This is neither a table nor really human readible. If you have many floating point numbers in your 2D array its really hard to even figure out what the columns are. – Felix Crazzolara Nov 18 '19 at 16:07
9

I dont know about a util library that would do this but you can use the String.format function to format the Strings as you want to display them. This is an example i quickly wrote up:

String[][] table = {{"Joe", "Bloggs", "18"},
        {"Steve", "Jobs", "20"},
        {"George", "Cloggs", "21"}};

    for(int i=0; i<3; i++){
        for(int j=0; j<3; j++){
            System.out.print(String.format("%20s", table[i][j]));
        }
        System.out.println("");
    }

This give the following output:

            Joe               Bloggs                  18
          Steve               Jobs                    20
         George               Cloggs                  21
lilroo
  • 2,928
  • 7
  • 25
  • 34
2

Since I stumbled upon this searching for a ready to use printer (but not only for Strings) I changed Lyubomyr Shaydariv's code written for the accepted answer a bit. Since this probably adds value to the question, I'll share it:

import static java.lang.String.format;

public final class PrettyPrinter {

    private static final char BORDER_KNOT = '+';
    private static final char HORIZONTAL_BORDER = '-';
    private static final char VERTICAL_BORDER = '|';

    private static final Printer<Object> DEFAULT = new Printer<Object>() {
        @Override
        public String print(Object obj) {
            return obj.toString();
        }
    };

    private static final String DEFAULT_AS_NULL = "(NULL)";

    public static String print(Object[][] table) {
        return print(table, DEFAULT);
    }

    public static <T> String print(T[][] table, Printer<T> printer) {
        if ( table == null ) {
            throw new IllegalArgumentException("No tabular data provided");
        }
        if ( table.length == 0 ) {
            return "";
        }
        if( printer == null ) {
        throw new IllegalArgumentException("No instance of Printer provided");
        }
        final int[] widths = new int[getMaxColumns(table)];
        adjustColumnWidths(table, widths, printer);
        return printPreparedTable(table, widths, getHorizontalBorder(widths), printer);
    }

    private static <T> String printPreparedTable(T[][] table, int widths[], String horizontalBorder, Printer<T> printer) {
        final int lineLength = horizontalBorder.length();
        StringBuilder sb = new StringBuilder();
        sb.append(horizontalBorder);
        sb.append('\n');
        for ( final T[] row : table ) {
            if ( row != null ) {
                sb.append(getRow(row, widths, lineLength, printer));
                sb.append('\n');
                sb.append(horizontalBorder);
                sb.append('\n');
            }
        }
        return sb.toString();
    }

    private static <T> String getRow(T[] row, int[] widths, int lineLength, Printer<T> printer) {
        final StringBuilder builder = new StringBuilder(lineLength).append(VERTICAL_BORDER);
        final int maxWidths = widths.length;
        for ( int i = 0; i < maxWidths; i++ ) {
            builder.append(padRight(getCellValue(safeGet(row, i, printer), printer), widths[i])).append(VERTICAL_BORDER);
        }
        return builder.toString();
    }

    private static String getHorizontalBorder(int[] widths) {
        final StringBuilder builder = new StringBuilder(256);
        builder.append(BORDER_KNOT);
        for ( final int w : widths ) {
            for ( int i = 0; i < w; i++ ) {
                builder.append(HORIZONTAL_BORDER);
            }
            builder.append(BORDER_KNOT);
        }
        return builder.toString();
    }

    private static int getMaxColumns(Object[][] rows) {
        int max = 0;
        for ( final Object[] row : rows ) {
            if ( row != null && row.length > max ) {
                max = row.length;
            }
        }
        return max;
    }

    private static <T> void adjustColumnWidths(T[][] rows, int[] widths, Printer<T> printer) {
        for ( final T[] row : rows ) {
            if ( row != null ) {
                for ( int c = 0; c < widths.length; c++ ) {
                    final String cv = getCellValue(safeGet(row, c, printer), printer);
                    final int l = cv.length();
                    if ( widths[c] < l ) {
                        widths[c] = l;
                    }
                }
            }
        }
    }

    private static <T> String padRight(String s, int n) {
        return format("%1$-" + n + "s", s);
    }

    private static <T> T safeGet(T[] array, int index, Printer<T> printer) {
        return index < array.length ? array[index] : null;
    }

    private static <T> String getCellValue(T value, Printer<T> printer) {
        return value == null ? DEFAULT_AS_NULL : printer.print(value);
    }

}

And Printer.java:

public interface Printer<T> {
    String print(T obj);
}

Usage:

    System.out.println(PrettyPrinter.print(some2dArray, new Printer<Integer>() {
        @Override
        public String print(Integer obj) {
            return obj.toString();
        }
    }));

Why the change to T? Well String was not enough, but I don't always want the same output obj.toString() has to offer, so I used the interface to be able to change that at will.

The second change is not providing the class with an outstream, well if you want to use the class with a Logger like (Log4j // Logback) you will definitly have a problem using streams.

Why the change to only support static calls? I wanted to use it for loggers, and making just one single call seemed the obvious choice.

Community
  • 1
  • 1
Sebastian van Wickern
  • 1,699
  • 3
  • 15
  • 31
  • Hi. Thanks for pointing at `T` -- I took only `String`s into account. Regarding the static method returning a `String`: I'd rather provide a writer callback injection or mimic a `PrintStream` to write to a logger, because usually it's not necessary to get the result of such a print, but it's necessary to write it somewhere without taking care of delegating the result to a proper object that can write. – Lyubomyr Shaydariv May 17 '13 at 15:41
  • Also note, that `String` is a general representational type. `T` isn't and it covers a single type for a field only, not a record. Because you might want to process a list of objects where each field could be converted to a `String`. If you want to pretty-print rows composed of a `List` or `Person[]` -- how do you convert then to print the `firstName` (`String`), `lastName` (`String`), `dateOfBirth` (`Date`) fields separately within `T`? You probably might propose a printer interface that returns String[] or whatever, but in the result it always goes `String[][]`. – Lyubomyr Shaydariv May 17 '13 at 16:10
  • I disagree with your first comment on PrintStream since that is incompatible with slf4j loggers, but that may be a holy war I'd rather not get into (as is the proposed static method). To answer your question, for each object you put into the PrettyPrinter the Printer interface is called, where you can define any conversion from T to String, which ends up in a lot more flexibility than the original. And you could iterate the list, if every field holds a list of Person's. If you do not specify a printer T.toString() is called. – Sebastian van Wickern May 21 '13 at 09:56
1

JakWharton has a nice solution https://github.com/JakeWharton/flip-tables and format String[], List of objects with reflection on property name and result sets. e.g.

List<Person> people = Arrays.asList(new Person("Foo", "Bar"), new Person("Kit", "Kat"));
System.out.println(FlipTableConverters.fromIterable(people, Person.class));
Shadi Moadad
  • 78
  • 1
  • 7