3
\$\begingroup\$

This program prints out a table in the console. Suggestions for improvements are welcome.

Example output

Name             Sex      Age
Klaus Ulbrecht   male     12
Dieter           male     14
Ursula           female   16

You can test and modify the program here.

import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;

class Main {
  private static List<List<String>> table; 

  public static void main(String[] args) {
    initTable();
    int spacing = 3;
    printTable(spacing);
  }

  private static void initTable() {
    List<String> row1 = Arrays.asList("Name", "Klaus Ulbrecht", "Dieter", "Ursula");
    List<String> row2 = Arrays.asList("Sex", "male", "male", "female");
    List<String> row3 = Arrays.asList("Age", "12", "14", "16");
    table = Arrays.asList(row1, row2, row3);
  }

  private static void printTable(int spacing) {
    List<Integer> maxLengths = findMaxLengths();

    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < table.get(0).size(); i++) {
      for (int j = 0; j < table.size(); j++) {
        String currentValue = table.get(j).get(i);
        sb.append(currentValue);
        for (int k = 0; k < (maxLengths.get(j) - currentValue.length() + spacing); k++) {
          sb.append(' ');
        }
      }
      sb.append('\n');
    }

    System.out.println(sb);
  }

  private static List<Integer> findMaxLengths() {
    List<Integer> maxLengths = new ArrayList<>();
    for (List<String> row : table) {
      int maxLength = 0;
      for (String value : row) {
        if (value.length() > maxLength) {
          maxLength = value.length();
        }
      }
      maxLengths.add(maxLength);
    }
    return maxLengths;
  }
}
\$\endgroup\$
  • \$\begingroup\$ is "being robust" one of your requirements? \$\endgroup\$ – Martin Frank Feb 11 at 9:09
  • \$\begingroup\$ I would say robustness is always a good thing! I am in the process of improving my code and adding a few features (CSV parsing, pseudo-graphics, etc.) \$\endgroup\$ – Vengeancos Feb 11 at 10:49
  • \$\begingroup\$ think what would happen if List<String> row1 = Arrays.asList("Name", "Klaus Ulbrecht", "Dieter"); - heck, we lost Ursula ... that's even a harmless case... but if you forget an entry in List<String> row2 = ... you are in serious problems ^^ \$\endgroup\$ – Martin Frank Feb 11 at 10:58
  • \$\begingroup\$ thanks for adding the output! \$\endgroup\$ – Martin Frank Feb 11 at 10:58
  • \$\begingroup\$ have you considered the usage of existing classes, even though they may be kind of "misplaced" (heck, swing in console???), but maybe have a look at docs.oracle.com/javase/7/docs/api/javax/swing/table/… \$\endgroup\$ – Martin Frank Feb 11 at 11:00
2
\$\begingroup\$

Hello and thank your for your code. I dont' know what kind of improvements you want but from an Object Oriented perspective I see some improvements.

The first one is to get rid of all the static methods and fields. You can easily move all your methods to an AsciiTable class with a print(OutputStream out):void method.

You can also use arrays instead of list and split the single argument to one for the columns titles and another for the content. This will clarify the initiliazation of your table.

Table table = new Table(
    new String[]{"Name", "Sex", "Age"},
    new Object[][]{
            {"Klaus Ulbrecht", "male", 12},
            {"Dieter", "male", 14},
            {"Ursula", "female", 16}
        });

As you can see I changed the order so that the cognitive load is reduced, by looking at the declaration we alrdeay have an idea of what are the titles and the content. We read for top to bottom, so it is more easier to have a declaration that already looks like that. This avoid your brain to switch the representation.

Another improvement that you can do is apply the separation of concerns principle by moving the "printing" away of the table. This is done easily with the builder where your Table is a director that drive a Format.

With this pattern you should be able to add another representation without changing the Table.

interface Format {
    void startHeader();
    void endHeader();

    void startBody();
    void endBody();

    void startRow();
    void endRow();

    void addColumn(Object value);
}

Format console = new Ascii(System.out, "   "); 
table.writeTo(console);

Now that your model (Table) is separated from the view (Format) you may discover that the model is quite simple and exists mainly to drive the view as the director in the builder pattern. But you can add new methods to filter or sort your table.

Table table = new Table(
    new String[]{"Name", "Sex", "Age"},
    new Object[][]{
            {"Klaus Ulbrecht", "male", 12},
            {"Dieter", "male", 14},
            {"Ursula", "female", 16}
    });
table.filter(1, "male"::equalsIgnoreCase)
     .sort(2, Comparator.naturalOrder())
     .writeTo(console);

Name             Sex    Age   
Klaus Ulbrecht   male   12    
Dieter           male   14          

If you don't want to migrate to the object oriented way you can still improve the robustness of your code by dealing with empty columns or rows.

[1] Single Responsibility Priniciple is part of the S.O.L.I.D. acronym. https://en.wikipedia.org/wiki/SOLID

[2] When I speak about the builder pattern I refer to the creational pattern. Not the "fluent way of writing". https://en.wikipedia.org/wiki/Builder_pattern

\$\endgroup\$
1
\$\begingroup\$

Printing arbitrary data as a table is a nice task of work for a utility function.

Such utility functions are usually defined in a utility class, which looks like this:

public final class Tables {
    private Tables() {}

    public static void print(List<List<String>> table, int spacing) {
        ...
    }
}

The name of the utility class usually takes the plural form of the main data ingredient. In this case that is a table, therefore Tables. The Java programming environment already defines similar utility classes named Collections and Arrays.

There is one important difference to your current code. All the variables needed by the Tables.print method are passed via parameters. There is no static field anywhere. This allows the print method to be called several times in the same moment and unambiguously lists all the data you have to provide when calling that method.

Citing from your code:

private static void initTable() {
  List<String> row1 = Arrays.asList("Name", "Klaus Ulbrecht", "Dieter", "Ursula");
  List<String> row2 = Arrays.asList("Sex", "male", "male", "female");
  List<String> row3 = Arrays.asList("Age", "12", "14", "16");
  table = Arrays.asList(row1, row2, row3);
}

You are mixing up the terms row and column here. A row in initTable will later be output as a column in printTable. That's confusing.

Several parts of your printTable method should be changed:

  • The table should be passed as a parameter, instead of being a static field in the Main class.
  • In the innermost for loop, the complicated expression is calculated several times, which is unnecessary. You should rather count from k downto 0: for (int k = ...; k > 0; k--) { ... }
  • The final System.out.println prints 2 newlines. One of the may or may not be desired. In the latter case, replace the println with print.

In the findMaxLengths method:

  • The table should be passed as a parameter, as above.
  • Instead of the if clause, you can just write maxLength = Math.max(maxLength, value.length());
\$\endgroup\$

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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