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

com.swirlds.common.formatting.TextTable Maven / Gradle / Ivy

Go to download

Swirlds is a software platform designed to build fully-distributed applications that harness the power of the cloud without servers. Now you can develop applications with fairness in decision making, speed, trust and reliability, at a fraction of the cost of traditional server-based platforms.

There is a newer version: 0.56.6
Show newest version
/*
 * Copyright (C) 2023-2024 Hedera Hashgraph, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.swirlds.common.formatting;

import static com.swirlds.common.formatting.HorizontalAlignment.ALIGNED_CENTER;
import static com.swirlds.common.formatting.HorizontalAlignment.ALIGNED_LEFT;
import static com.swirlds.common.formatting.StringFormattingUtils.repeatedChar;
import static com.swirlds.common.formatting.TextEffect.applyEffects;
import static com.swirlds.common.formatting.TextEffect.getPrintableTextLength;

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * 

* Utility class for formatting and printing an ASCII table. *

* *

* See the TextEffect-Colors.png for a screenshot of all effects rendered by intellij on a macbook. *

*/ public class TextTable { private static final char PADDING = ' '; private static final char CROSS_JUNCTION = '┼'; private static final char LEFT_JUNCTION = '┠'; private static final char LEFT_HEADER_JUNCTION = '┣'; private static final char RIGHT_JUNCTION = '┨'; private static final char RIGHT_HEADER_JUNCTION = '┫'; private static final char BOTTOM_JUNCTION = '┷'; private static final char TOP_JUNCTION = '┯'; private static final char TOP_LEFT_CORNER = '┏'; private static final char TOP_RIGHT_CORNER = '┓'; private static final char BOTTOM_LEFT_CORNER = '┗'; private static final char BOTTOM_RIGHT_CORNER = '┛'; private static final char HORIZONTAL_BAR = '─'; private static final char THICK_HORIZONTAL_BAR = '━'; private static final char VERTICAL_BAR = '│'; private static final char THICK_VERTICAL_BAR = '┃'; private static final char NEWLINE = '\n'; private String title; private final List> rows = new ArrayList<>(); private boolean bordersEnabled = true; private int extraPadding = 0; /** * Describes the position of a cell. */ private record Cell(int row, int column) {} private final Map> rowEffects = new HashMap<>(); private final Map> columnEffects = new HashMap<>(); private final Map> cellEffects = new HashMap<>(); private final List titleEffects = new ArrayList<>(); private final List globalCellEffects = new ArrayList<>(); private final List borderEffects = new ArrayList<>(); private final Map rowHorizontalAlignments = new HashMap<>(); private final Map columnHorizontalAlignments = new HashMap<>(); private final Map cellHorizontalAlignments = new HashMap<>(); private HorizontalAlignment titleHorizontalAlignment = ALIGNED_CENTER; private HorizontalAlignment globalHorizontalAlignment = ALIGNED_LEFT; /** * Create a new text table. */ public TextTable() {} /** * Enable or disable borders. * * @param bordersEnabled * whether borders should be enabled * @return this object */ @NonNull public TextTable setBordersEnabled(final boolean bordersEnabled) { this.bordersEnabled = bordersEnabled; return this; } /** * Enable extra padding for each column. * * @param extraPadding * the number of extra spaces to add to each column * @return this object */ @NonNull public TextTable setExtraPadding(final int extraPadding) { this.extraPadding = extraPadding; return this; } /** * Set the title of the table. * * @param title * the title of the table * @return this object */ @NonNull public TextTable setTitle(@NonNull final String title) { this.title = title; return this; } /** * Add a row to the table. * * @param row * a single row * @return this object */ @SuppressWarnings("DuplicatedCode") @NonNull public TextTable addRow(@Nullable final Object... row) { if (row != null) { final List rowString = new ArrayList<>(); for (final Object o : row) { rowString.add(o == null ? "null" : o.toString()); } rows.add(rowString); } return this; } /** * Add to the current row. * * @param elements * the elements to add to the current row * @return this object */ @NonNull public TextTable addToRow(@NonNull final Object... elements) { final List row; if (rows.isEmpty()) { row = new ArrayList<>(); rows.add(row); } else { row = rows.get(rows.size() - 1); } for (final Object o : elements) { row.add(o == null ? "null" : o.toString()); } return this; } /** *

* Add text effects to a row. Has no effect if there is no data in the specified row when the table is rendered. *

* *

* Effects are applied in the following order: *

*
    *
  1. global
  2. *
  3. row
  4. *
  5. column
  6. *
  7. cell
  8. *
* * @param row * the target row * @param effects * zero or more effects to add for the row * @return this object */ @NonNull public TextTable addRowEffects(final int row, @Nullable final TextEffect... effects) { if (effects == null) { return this; } final List effectList = rowEffects.computeIfAbsent(row, k -> new ArrayList<>()); effectList.addAll(Arrays.asList(effects)); return this; } /** *

* Add text effects to a column. Has no effect if there is no data in the specified column when the table is * rendered. *

* *

* Effects are applied in the following order: *

*
    *
  1. global
  2. *
  3. row
  4. *
  5. column
  6. *
  7. cell
  8. *
* * @param column * the target column * @param effects * zero or more effects to add for the column * @return this object */ @NonNull public TextTable addColumnEffects(final int column, @Nullable final TextEffect... effects) { if (effects == null) { return this; } final List effectList = columnEffects.computeIfAbsent(column, k -> new ArrayList<>()); effectList.addAll(Arrays.asList(effects)); return this; } /** *

* Add text effects to a cell. Has no effect if there is no data in the specified cell when the table is rendered. *

* *

* Effects are applied in the following order: *

*
    *
  1. global
  2. *
  3. row
  4. *
  5. column
  6. *
  7. cell
  8. *
* * @param row * the target row * @param column * the target column * @param effects * zero or more effects to add for the cell * @return this object */ @NonNull public TextTable addCellEffects(final int row, final int column, @Nullable final TextEffect... effects) { if (effects == null) { return this; } final Cell cell = new Cell(row, column); final List effectList = cellEffects.computeIfAbsent(cell, k -> new ArrayList<>()); effectList.addAll(Arrays.asList(effects)); return this; } /** * Add text effects to the title. * * @param effects * zero or more effects to add to the title * @return this object */ @NonNull public TextTable addTitleEffects(@Nullable final TextEffect... effects) { if (effects == null) { return this; } titleEffects.addAll(Arrays.asList(effects)); return this; } /** *

* Add text effects to all cells. Does not affect the header, title, or border. *

* *

* Effects are applied in the following order: *

*
    *
  1. global
  2. *
  3. row
  4. *
  5. column
  6. *
  7. cell
  8. *
* * @param effects * zero or more effects to add to all cells * @return this object */ @NonNull public TextTable addGlobalCellEffects(@Nullable final TextEffect... effects) { if (effects == null) { return this; } globalCellEffects.addAll(Arrays.asList(effects)); return this; } /** * Add text effects to cell borders. * * @param effects * zero or more effects to add to borders * @return this object */ @NonNull public TextTable addBorderEffects(@Nullable final TextEffect... effects) { if (effects == null) { return this; } borderEffects.addAll(Arrays.asList(effects)); return this; } /** *

* Set the horizontal alignment for a row. *

* *

* Alignments are applied in the following order: *

*
    *
  1. global
  2. *
  3. row
  4. *
  5. column
  6. *
  7. cell
  8. *
* * @param row * the row index * @param alignment * the alignment of the row * @return this object */ @NonNull public TextTable setRowHorizontalAlignment(final int row, @NonNull final HorizontalAlignment alignment) { Objects.requireNonNull(alignment, "alignment must not be null"); rowHorizontalAlignments.put(row, alignment); return this; } /** *

* Set the horizontal alignment for a column. *

* *

* Alignments are applied in the following order: *

*
    *
  1. global
  2. *
  3. row
  4. *
  5. column
  6. *
  7. cell
  8. *
* * @param column * the column index * @param alignment * the alignment of the column * @return this object */ @NonNull public TextTable setColumnHorizontalAlignment(final int column, @NonNull final HorizontalAlignment alignment) { Objects.requireNonNull(alignment, "alignment must not be null"); columnHorizontalAlignments.put(column, alignment); return this; } /** *

* Set the horizontal alignment for a cell. *

* *

* Alignments are applied in the following order: *

*
    *
  1. global
  2. *
  3. row
  4. *
  5. column
  6. *
  7. cell
  8. *
* * @param row * the row index * @param column * the column index * @param alignment * the alignment of the cell * @return this object */ @NonNull public TextTable setCellHorizontalAlignment( final int row, final int column, @NonNull final HorizontalAlignment alignment) { Objects.requireNonNull(alignment, "alignment must not be null"); cellHorizontalAlignments.put(new Cell(row, column), alignment); return this; } /** * Set the horizontal alignment for the title. If not specified then the global alignment is used. * * @param alignment * the alignment of the title * @return this object */ @NonNull public TextTable setTitleHorizontalAlignment(@NonNull final HorizontalAlignment alignment) { Objects.requireNonNull(alignment, "alignment must not be null"); titleHorizontalAlignment = alignment; return this; } /** *

* Set the default horizontal alignment for the entire table *

* *

* Alignments are applied in the following order: *

*
    *
  1. global
  2. *
  3. row
  4. *
  5. column
  6. *
  7. cell
  8. *
* * @param alignment * the alignment for the entire table * @return this object */ @NonNull public TextTable setGlobalHorizontalAlignment(@NonNull final HorizontalAlignment alignment) { Objects.requireNonNull(alignment, "alignment must not be null"); globalHorizontalAlignment = alignment; return this; } /** * Format cell data. */ @NonNull private String formatCellData(final int row, final int column, @NonNull final String cellData) { final List effects = new ArrayList<>(globalCellEffects); if (rowEffects.containsKey(row)) { effects.addAll(rowEffects.get(row)); } if (columnEffects.containsKey(column)) { effects.addAll(columnEffects.get(column)); } final Cell cell = new Cell(row, column); if (cellEffects.containsKey(cell)) { effects.addAll(cellEffects.get(cell)); } if (effects.isEmpty()) { return cellData; } else { return TextEffect.applyEffects(cellData, effects); } } /** * Format a cell with left/right alignment. */ private void alignCellData( @NonNull final StringBuilder sb, final int row, final int column, @NonNull final String cellData, final int desiredWidth, final boolean isLastColumn) { final HorizontalAlignment alignment; final Cell cell = new Cell(row, column); if (cellHorizontalAlignments.containsKey(cell)) { alignment = cellHorizontalAlignments.get(cell); } else if (columnHorizontalAlignments.containsKey(column)) { alignment = columnHorizontalAlignments.get(column); } else if (rowHorizontalAlignments.containsKey(row)) { alignment = rowHorizontalAlignments.get(row); } else { alignment = globalHorizontalAlignment; } final boolean trailingPadding = bordersEnabled || !isLastColumn; alignment.pad(sb, cellData, ' ', desiredWidth, trailingPadding); } /** * Format a border character(s) and write it to a string builder. */ private void writeBorder(@NonNull final StringBuilder sb, @NonNull final String border) { if (borderEffects.isEmpty()) { sb.append(border); } else { applyEffects(sb, border, borderEffects); } } /** * Format a border character and write it to a string builder. */ private void writeBorder(@NonNull final StringBuilder sb, final char border) { writeBorder(sb, String.valueOf(border)); } /** * Expand planned column widths to fit a given row. After all rows have been processed this way, * the column widths list will contain the proper width for each column. * * @param row * the row that needs to be fitted into the table */ private static void expandColumnWidthsForRow( @NonNull final List columnWidths, @NonNull final List row) { for (int column = 0; column < row.size(); column++) { final int columnWidth = getPrintableTextLength(row.get(column)); if (columnWidths.size() <= column) { columnWidths.add(columnWidth); } else { columnWidths.set(column, Math.max(columnWidths.get(column), columnWidth)); } } } /** * Compute the width for each column. * * @return a list of widths indexed by column */ private List computeColumnWidths() { final List columnWidths = new ArrayList<>(); for (final List row : rows) { expandColumnWidthsForRow(columnWidths, row); } return columnWidths; } /** * Generate the top of the table. */ private void generateTopLine( @NonNull final StringBuilder sb, @NonNull final List columnWidths, final int columnWidthSum) { if (!bordersEnabled) { return; } writeBorder(sb, TOP_LEFT_CORNER); if (title != null) { writeBorder(sb, repeatedChar(THICK_HORIZONTAL_BAR, columnWidthSum + columnWidths.size() * 3 - 1)); } else { for (int columnIndex = 0; columnIndex < columnWidths.size(); columnIndex++) { writeBorder(sb, repeatedChar(THICK_HORIZONTAL_BAR, columnWidths.get(columnIndex) + 2 + extraPadding)); if (columnIndex + 1 < columnWidths.size()) { writeBorder(sb, TOP_JUNCTION); } } } writeBorder(sb, TOP_RIGHT_CORNER); sb.append(NEWLINE); } /** * Generate the line containing the title. */ private void generateTitleLine(@NonNull final StringBuilder sb, final int columnWidthSum, final int columnCount) { if (title == null) { return; } if (bordersEnabled) { writeBorder(sb, THICK_VERTICAL_BAR); sb.append(PADDING); } final String formattedTitle = applyEffects(title, titleEffects); final int titleWidth; if (bordersEnabled) { titleWidth = columnWidthSum + columnCount * 3 - 3; } else { titleWidth = columnWidthSum + columnCount * 3 - 1; } titleHorizontalAlignment.pad(sb, formattedTitle, ' ', titleWidth, bordersEnabled); if (bordersEnabled) { sb.append(PADDING); writeBorder(sb, THICK_VERTICAL_BAR); sb.append(NEWLINE); } } /** * Generate the line between the title and the headers. */ private void generateLineBelowTitle(@NonNull final StringBuilder sb, @NonNull final List columnWidths) { if (title == null || !bordersEnabled) { return; } writeBorder(sb, LEFT_HEADER_JUNCTION); for (int columnIndex = 0; columnIndex < columnWidths.size(); columnIndex++) { writeBorder(sb, repeatedChar(THICK_HORIZONTAL_BAR, columnWidths.get(columnIndex) + 2 + extraPadding)); if (columnIndex + 1 < columnWidths.size()) { writeBorder(sb, TOP_JUNCTION); } } writeBorder(sb, RIGHT_HEADER_JUNCTION); sb.append(NEWLINE); } /** * Generate a row containing column data. */ private void generateDataRow( @NonNull final StringBuilder sb, final int row, @NonNull final List columnWidths) { final List rowData = rows.get(row); if (bordersEnabled) { writeBorder(sb, THICK_VERTICAL_BAR); } for (int column = 0; column < columnWidths.size(); column++) { if (bordersEnabled) { sb.append(PADDING); } final String cellData = column < rowData.size() ? rowData.get(column) : ""; final String formattedCellData = formatCellData(row, column, cellData); final boolean isLastColumn = column == columnWidths.size() - 1; alignCellData(sb, row, column, formattedCellData, columnWidths.get(column), isLastColumn); if (bordersEnabled || !isLastColumn) { sb.append(repeatedChar(PADDING, extraPadding + 1)); } if (bordersEnabled && column + 1 < columnWidths.size()) { writeBorder(sb, VERTICAL_BAR); } } if (bordersEnabled) { writeBorder(sb, THICK_VERTICAL_BAR); } sb.append(NEWLINE); } /** * Generate the line below a row containing data. */ private void generateLineBelowDataRow(@NonNull final StringBuilder sb, @NonNull final List columnWidths) { if (!bordersEnabled) { return; } writeBorder(sb, LEFT_JUNCTION); for (int columnIndex = 0; columnIndex < columnWidths.size(); columnIndex++) { writeBorder(sb, repeatedChar(HORIZONTAL_BAR, columnWidths.get(columnIndex) + 2 + extraPadding)); if (columnIndex + 1 < columnWidths.size()) { writeBorder(sb, CROSS_JUNCTION); } } writeBorder(sb, RIGHT_JUNCTION); sb.append(NEWLINE); } /** * Generate the rows in the table. */ private void generateRows(@NonNull final StringBuilder sb, @NonNull final List columnWidths) { for (int row = 0; row < rows.size(); row++) { generateDataRow(sb, row, columnWidths); // Line below row if (row + 1 < rows.size()) { generateLineBelowDataRow(sb, columnWidths); } } } /** * Generate the last line in the table. */ private void generateBottomLine(@NonNull final StringBuilder sb, @NonNull final List columnWidths) { if (!bordersEnabled) { return; } writeBorder(sb, BOTTOM_LEFT_CORNER); for (int columnIndex = 0; columnIndex < columnWidths.size(); columnIndex++) { writeBorder(sb, repeatedChar(THICK_HORIZONTAL_BAR, columnWidths.get(columnIndex) + 2 + extraPadding)); if (columnIndex + 1 < columnWidths.size()) { writeBorder(sb, BOTTOM_JUNCTION); } } writeBorder(sb, BOTTOM_RIGHT_CORNER); } /** * If the title is really long then expand the last column to fill the space. */ private void expandLastColumnIfNeeded(@NonNull final List columnWidths) { if (title == null) { return; } int columnWidthSum = 0; for (final int columnWidth : columnWidths) { columnWidthSum += columnWidth; } final int titleLength = getPrintableTextLength(title); final int minimumWidth = titleLength + columnWidths.size() * 3 - 3; if (columnWidthSum < minimumWidth) { // Title is too wide, expand a column to balance it out final int expansion = minimumWidth - columnWidthSum; final int lastIndex = columnWidths.size() - 1; columnWidths.set(lastIndex, columnWidths.get(lastIndex) + expansion); } } /** * Render this table to a string builder. * * @param sb * the string builder to add to */ public void render(@NonNull final StringBuilder sb) { final List columnWidths = computeColumnWidths(); expandLastColumnIfNeeded(columnWidths); int columnWidthSum = 0; for (final int columnWidth : columnWidths) { columnWidthSum += columnWidth + extraPadding; } generateTopLine(sb, columnWidths, columnWidthSum); generateTitleLine(sb, columnWidthSum, columnWidths.size()); generateLineBelowTitle(sb, columnWidths); generateRows(sb, columnWidths); generateBottomLine(sb, columnWidths); } /** * Render this table to a string. * * @return the rendered table */ @NonNull public String render() { final StringBuilder sb = new StringBuilder(); render(sb); return sb.toString(); } /** * {@inheritDoc} */ @NonNull @Override public String toString() { return render(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy