All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mitchtalmadge.asciidata.table.ASCIITable Maven / Gradle / Ivy

There is a newer version: 4.15.102
Show newest version
package com.mitchtalmadge.asciidata.table;

import com.mitchtalmadge.asciidata.table.formats.TableFormatAbstract;
import com.mitchtalmadge.asciidata.table.formats.UTF8TableFormat;

/**
 * A table created entirely from ASCII characters, such as pipes.
 * Printable to a console, chat server, or anywhere else where text-style tables are convenient.
 *
 * @author MitchTalmadge
 * @author JakeWharton
 * @author bitsofinfo
 */
public class ASCIITable {

    private final String[] headers;
    private final String[][] data;
    private final int columnsCount;
    private final int[] columnWidths;
    private final int emptyWidth;
    private final String emptyMessage = "(empty)";

    /**
     * How the table will be displayed. Defines which characters to be used.
     * Defaults to {@link UTF8TableFormat}.
     */
    private TableFormatAbstract tableFormat = new UTF8TableFormat();

    /**
     * Creates a new table using the given headers and data.
     *
     * @param headers The headers of the table. Each index is a new column, in order from left to right.
     * @param data    The data of the table, in the format String[row][column]. Newlines are allowed.
     * @return A new ASCIITable instance that can be printed using the {@link ASCIITable#toString()}.
     */
    @SuppressWarnings("WeakerAccess")
    public static ASCIITable fromData(String[] headers, String[][] data) {
        // Ensure we have headers.
        if (headers == null)
            throw new IllegalArgumentException("The table headers array is null.");
        if (headers.length == 0)
            throw new IllegalArgumentException("No headers were supplied.");

        // Set the data to empty if it is null.
        if (data == null)
            data = new String[0][0];

        // Create an ASCIITable instance.
        return new ASCIITable(headers, data);
    }

    /**
     * Constructs a new ASCIITable from the given headers and data.
     *
     * @param headers The headers of the table.
     * @param data    The data of the table.
     */
    private ASCIITable(String[] headers, String[][] data) {
        this.headers = headers;
        this.data = data;

        // The number of columns in the table is equivalent to the number of headers.
        columnsCount = headers.length;

        // Calculate the max width of each column in the table.
        columnWidths = new int[columnsCount];
        for (int row = 0; row < data.length + 1; row++) {
            // The first row is for the headers.
            String[] rowData = row == 0 ? headers : data[row - 1];

            // Make sure we have enough columns.
            if (rowData.length != columnsCount)
                throw new IllegalArgumentException("The number of columns in row " + (row - 1) + " (" + rowData.length + ") " +
                        "do not match the number of headers (" + columnsCount + ")");

            // Iterate over each column in the row to get its width, and compare it to the maximum.
            for (int column = 0; column < columnsCount; column++) {
                // Check the length of each line in the cell.
                for (String rowDataLine : rowData[column].split("\\n"))
                    // Compare to the current max width.
                    columnWidths[column] = Math.max(columnWidths[column], rowDataLine.length());
            }
        }

        // Determine the width of everything including borders.
        // This is to be used in case there is no data and we must write the empty message to the table.

        // Account for borders
        int emptyWidth = 3 * (columnsCount - 1);
        // Add the width of each column.
        for (int columnWidth : columnWidths) {
            emptyWidth += columnWidth;
        }
        this.emptyWidth = emptyWidth;

        // Make sure we have enough space for the empty message.
        if (emptyWidth < emptyMessage.length()) {
            columnWidths[columnsCount - 1] += emptyMessage.length() - emptyWidth;
        }
    }

    /**
     * Changes the format of the table (how it will be displayed; which characters to use) to the provided format.
     *
     * @param tableFormat The format to use. By default, the table will already use {@link UTF8TableFormat}.
     * @return This ASCIITable instance.
     */
    public ASCIITable withTableFormat(TableFormatAbstract tableFormat) {
        this.tableFormat = tableFormat;
        return this;
    }

    @Override
    public String toString() {
        StringBuilder output = new StringBuilder();

        // Append the table's top horizontal divider.
        appendHorizontalDivider(output,
                tableFormat.getTopLeftCorner(),
                tableFormat.getHorizontalBorderFill(true, false),
                tableFormat.getTopEdgeBorderDivider(),
                tableFormat.getTopRightCorner());

        // Append the headers of the table.
        appendRow(output, headers);

        // Check if the data is empty, in which case, we will only write the empty message into the table contents.
        if (data.length == 0) {
            // Horizontal divider below the headers
            appendHorizontalDivider(output, tableFormat.getLeftEdgeBorderDivider(true),
                    tableFormat.getHorizontalBorderFill(false, true),
                    tableFormat.getCross(true, true),
                    tableFormat.getRightEdgeBorderDivider(true));

            // Empty message row
            output.append(tableFormat.getVerticalBorderFill(true))
                    .append(pad(emptyWidth, emptyMessage))
                    .append(tableFormat.getVerticalBorderFill(true))
                    .append('\n');

            // Horizontal divider at the bottom of the table.
            appendHorizontalDivider(output,
                    tableFormat.getBottomLeftCorner(),
                    tableFormat.getHorizontalBorderFill(true, false),
                    tableFormat.getHorizontalBorderFill(true, false),
                    tableFormat.getBottomRightCorner());

            return output.toString();
        }

        // The data is not empty, so iterate over each row.
        for (int row = 0; row < data.length; row++) {

            // The first row has a different style of border than the others.
            if (row == 0)
                appendHorizontalDivider(output,
                        tableFormat.getLeftEdgeBorderDivider(true),
                        tableFormat.getHorizontalBorderFill(false, true),
                        tableFormat.getCross(true, false),
                        tableFormat.getRightEdgeBorderDivider(true));
            else
                appendHorizontalDivider(output,
                        tableFormat.getLeftEdgeBorderDivider(false),
                        tableFormat.getHorizontalBorderFill(false, false),
                        tableFormat.getCross(false, false),
                        tableFormat.getRightEdgeBorderDivider(false));

            // Append the data for the current row.
            appendRow(output, data[row]);
        }

        // Horizontal divider at the bottom of the table.
        appendHorizontalDivider(output,
                tableFormat.getBottomLeftCorner(),
                tableFormat.getHorizontalBorderFill(true, false),
                tableFormat.getBottomEdgeBorderDivider(),
                tableFormat.getBottomRightCorner());

        return output.toString();
    }

    /**
     * Appends the given data as a row to the output with appropriate borders.
     *
     * @param output The output to append to.
     * @param data   The data of the row to append. Each index corresponds to a column.
     */
    private void appendRow(StringBuilder output, String[] data) {
        // Step 1: Determine the row height from the maximum number of lines out of each cell.
        int rowHeight = 0;
        for (int column = 0; column < columnsCount; column++) {
            // The height of this cell.
            int cellHeight = data[column].split("\\n").length;
            // Choose the greatest.
            rowHeight = Math.max(rowHeight, cellHeight);
        }

        // Step 2: Append the data to the output.
        // Iterate over each line of text, using the row height calculated earlier.
        for (int line = 0; line < rowHeight; line++) {

            // For each column...
            for (int column = 0; column < columnsCount; column++) {

                // Either add the left or the middle borders, depending on the location of the column.
                output.append(tableFormat.getVerticalBorderFill(column == 0));

                // Split the data on its newlines to determine the contents of each line in the column.
                String[] cellLines = data[column].split("\\n");

                // Decide what to put into this column. Use empty data if there is no specific data for this column.
                String cellLine = line < cellLines.length ? cellLines[line] : "";

                // Pad and append the data.
                output.append(pad(columnWidths[column], cellLine));
            }

            // Add the right border.
            output.append(tableFormat.getVerticalBorderFill(true)).append('\n');
        }
    }

    /**
     * Appends a horizontal divider to the output using the given characters.
     * 

* Example output: (L = Left, F = Fill, M = Middle, R = Right) * ╚════╧═══════╧════╧═══════╝ * LFFFMFFFFFFMFFFMFFFFFFR * * @param output The output to append to. * @param left The left border character. * @param fill The fill border character. * @param middle The middle border character. * @param right The right border character. */ private void appendHorizontalDivider(StringBuilder output, char left, char fill, char middle, char right) { // For each column... for (int column = 0; column < columnsCount; column++) { // Either add the left or the middle borders, depending on the location of the column. output.append(column == 0 ? left : middle); // For the contents of the column, create a padding of the correct width and replace it with the fill border. output.append(pad(columnWidths[column], "").replace(' ', fill)); } // Add the right border output.append(right).append('\n'); } /** * Pads the given data to the specified width using spaces. * * @param width The width desired. * @param data The data to pad. * @return The data, padded with spaces to the given width. */ private static String pad(int width, String data) { return String.format(" %1$-" + width + "s ", data); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy