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

org.sentrysoftware.metricshub.engine.common.helpers.TextTableHelper Maven / Gradle / Ivy

There is a newer version: 0.9.06
Show newest version
package org.sentrysoftware.metricshub.engine.common.helpers;

/*-
 * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
 * MetricsHub Engine
 * ჻჻჻჻჻჻
 * Copyright 2023 - 2024 Sentry Software
 * ჻჻჻჻჻჻
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see .
 * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
 */

import static org.sentrysoftware.metricshub.engine.common.helpers.MetricsHubConstants.TABLE_SEP;
import static org.sentrysoftware.metricshub.engine.common.helpers.MetricsHubConstants.WHITE_SPACE;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.springframework.util.Assert;

/**
 * Helper class providing methods for working with text tables.
 */
public class TextTableHelper {

	private static final String N_A = "N/A";

	private TextTableHelper() {}

	private static final int PADDING_SIZE = 1;
	private static final String NEW_LINE = "\n";
	private static final String TABLE_JOINT_SYMBOL = "+";
	private static final String TABLE_VALUE_SPLIT_SYMBOL = "|";
	private static final String TABLE_BORDER_SYMBOL = "-";

	/**
	 * Creates a text table for the given rows.

* Each row is designed as a {@link List} of {@link String} values.

* The headers are generated as Column 1, Column 2, ..., * with the number of columns being based on the number of columns in the longest row. * * @param rows The {@link List} of rows. * * @return A formatted text table representing the given rows. */ public static String generateTextTable(List> rows) { if (rows == null || rows.isEmpty()) { return ""; } List longestRow = null; for (List row : rows) { longestRow = longestRow == null || (row != null && row.size() > longestRow.size()) ? row : longestRow; } if (longestRow == null || longestRow.isEmpty()) { return ""; } List headers = IntStream .range(1, longestRow.size() + 1) .mapToObj(index -> new TableHeader(String.format("Column %d", index), TextDataType.STRING)) .collect(Collectors.toList()); //NOSONAR return generateTextTable(headers, rows); } /** * Creates a text table for the given rows.

* Each row is designed as a {@link List} of {@link String} values.

* The headers are generated based on semiColonSeparatedColumns (e.g. : column1;column2). * * @param semiColonSeparatedColumns A semicolon-separated list of column names. * @param rows The {@link List} of rows. * * @return A formatted text table representing the given rows, * with the given column names as the headers. */ public static String generateTextTable(String semiColonSeparatedColumns, List> rows) { return (semiColonSeparatedColumns == null || semiColonSeparatedColumns.isBlank()) ? generateTextTable(rows) : generateTextTable(semiColonSeparatedColumns.split(TABLE_SEP), rows); } /** * Creates a text table for the given rows.

* Each row is designed as a {@link List} of {@link String} values.

* The headers are generated based on columns (e.g. : new String[] {column1, column2}). * * @param columns An array of column names. * @param rows The {@link List} of rows. * * @return A formatted text table representing the given rows, * with the given column names as the headers. */ public static String generateTextTable(String[] columns, List> rows) { if (columns == null || columns.length == 0) { return generateTextTable(rows); } List headers = Arrays .stream(columns) .map(columnName -> new TableHeader(columnName, TextDataType.STRING)) .collect(Collectors.toList()); //NOSONAR return generateTextTable(headers, rows); } /** * Creates a text table for the given rows.

* Each row is designed as a {@link List} of {@link String} values.

* The headers are generated based on columns (e.g. : Arrays.asList(column1, column2)). * * @param columns An {@link Collection} of column names. * @param rows The {@link List} of rows. * * @return A formatted text table representing the given rows, * with the given column names as the headers. */ public static String generateTextTable(Collection columns, List> rows) { if (columns == null || columns.isEmpty()) { return generateTextTable(rows); } List headers = columns .stream() .map(columnName -> new TableHeader(columnName, TextDataType.STRING)) .collect(Collectors.toList()); //NOSONAR return generateTextTable(headers, rows); } /** * Create a text table for the given headers and the * corresponding rows.
* Each row is designed as a {@link List} of {@link String} values. *
    *
  • This method expects non-null headers {@link List} and non-null rows {@link List}
  • *
  • headers {@link List} cannot contain null values
  • *
  • The row {@link List} elements order is important and must match the same order as on the headers {@link List}
  • *
  • A null row is ignored
  • *
  • A row can be empty or having a size less than the headers size
  • *
  • Extra cells in a row are ignored
  • *
* * @param headers {@link List} of columns we wish to insert in the text table * @param rows {@link List} of {@link List} elements * @return Text table */ public static String generateTextTable(final List headers, List> rows) { checkArguments(headers, rows); rows = cleanRows(rows, headers.size()); final StringBuilder stringBuilder = new StringBuilder(); final Map columnMaxWidthMapping = getMaximumWidthOfTable(headers, rows); // Open the table with a new row line createRowLine(stringBuilder, headers.size(), columnMaxWidthMapping); stringBuilder.append(NEW_LINE); // Create the header line for (int headerIndex = 0; headerIndex < headers.size(); headerIndex++) { fillCell( stringBuilder, headers.get(headerIndex).getTitle(), headerIndex, columnMaxWidthMapping, headers.get(headerIndex).getType() ); } stringBuilder.append(NEW_LINE); // Create a new row line to separate the headers and the rows createRowLine(stringBuilder, headers.size(), columnMaxWidthMapping); // Loop over each row to append them in the current table for (List row : rows) { stringBuilder.append(NEW_LINE); // Append each cell the row for (int cellIndex = 0; cellIndex < row.size(); cellIndex++) { fillCell(stringBuilder, row.get(cellIndex), cellIndex, columnMaxWidthMapping, headers.get(cellIndex).getType()); } } stringBuilder.append(NEW_LINE); // Close the table with a new row line createRowLine(stringBuilder, headers.size(), columnMaxWidthMapping); return stringBuilder.toString(); } /** * Remove null rows clean each row * @param rows we wish to clean * @param headersSize the size of the headers * @return new cleaned rows */ private static List> cleanRows(final List> rows, final int headersSize) { return rows.stream().filter(Objects::nonNull).map(row -> cleanRow(row, headersSize)).collect(Collectors.toList()); //NOSONAR } /** * Clean the given row based on the given headersSize *
    *
  • Replace null cells by "N/A"
  • *
  • Create missing cells with "N/A"
  • *
  • Remove extra cells
  • *
* @param row we wish to clean * @param headersSize the size of the headers * @return new cleaned row */ private static List cleanRow(final List row, final int headersSize) { // Creating a copy of the row ArrayList result = new ArrayList<>(row); // Replacing null cells // noinspection ResultOfMethodCallIgnored Collections.replaceAll(result, null, N_A); // Creating missing cells if (result.size() < headersSize) { return Stream .concat(result.stream(), Stream.generate(() -> N_A).limit((long) headersSize - result.size())) .collect(Collectors.toList()); //NOSONAR } else if (result.size() > headersSize) { // Removing extra cells return result.stream().limit(headersSize).collect(Collectors.toList()); //NOSONAR } return result; } private static void checkArguments(final List headers, final List> rows) { Assert.notNull(headers, "headers cannot be null."); Assert.notNull(rows, "rows cannot be null."); final int indexOfNull = headers.indexOf(null); Assert.isTrue(indexOfNull == -1, () -> String.format("Header at index '%d' cannot be null.", indexOfNull)); } /** * A row line is filled with {@link #TABLE_BORDER_SYMBOL} and joined with the {@link #TABLE_JOINT_SYMBOL}.
* E.g: +---------+---------------+--------+ * @param stringBuilder the {@link StringBuilder} we wish to update * @param headersSize the number of columns * @param columnMaxWidthMapping maximum size for each column */ private static void createRowLine( final StringBuilder stringBuilder, final int headersSize, Map columnMaxWidthMapping ) { for (int i = 0; i < headersSize; i++) { // First start with the table joint e.g. '+' character if (i == 0) { stringBuilder.append(TABLE_JOINT_SYMBOL); } // Then append the table border character using the max column size + the // padding twice since the values will be prefixed and suffixed with the padding // size stringBuilder.append(TABLE_BORDER_SYMBOL.repeat(columnMaxWidthMapping.get(i) + PADDING_SIZE * 2)); // Finally, append the table joint stringBuilder.append(TABLE_JOINT_SYMBOL); } } /** * Get the maximum width for each column by comparing the header lengths and row cells lengths. * @param headers {@link List} of columns we wish to compare * @param rows {@link List} of {@link List} elements * @return {@link Map} where each column index defines maximum length. */ private static Map getMaximumWidthOfTable( final List headers, final List> rows ) { final Map columnMaxWidthMapping = new HashMap<>(); // Initialize the map with header sizes as max lengths for (int columnIndex = 0; columnIndex < headers.size(); columnIndex++) { columnMaxWidthMapping.put(columnIndex, headers.get(columnIndex).getTitle().length()); } // Loop over all the row cells and determine the maximum size for each column index for (final List row : rows) { for (int columnIndex = 0; columnIndex < row.size(); columnIndex++) { if (row.get(columnIndex).length() > columnMaxWidthMapping.get(columnIndex)) { columnMaxWidthMapping.put(columnIndex, row.get(columnIndex).length()); } } } return columnMaxWidthMapping; } /** * The optimum cell padding is used for cells with data length less than the maximum column length. * @param cellIndex the index of the cells. i.e. the column index * @param dataLength the length of the cell data * @param columnMaxWidthMapping max column lengths * @return a cell padding size to append before or after the data value */ private static int getOptimumCellPadding( final int cellIndex, int dataLength, final Map columnMaxWidthMapping ) { int cellPaddingSize = PADDING_SIZE; if (dataLength < columnMaxWidthMapping.get(cellIndex)) { cellPaddingSize = PADDING_SIZE + columnMaxWidthMapping.get(cellIndex) - dataLength; } return cellPaddingSize; } /** * Simply append white spaces on the given stringBuilder * @param stringBuilder {@link StringBuilder} we wish to update * @param times number of append operations we wish to perform */ private static void fillSpace(final StringBuilder stringBuilder, final int times) { stringBuilder.append(WHITE_SPACE.repeat(times)); } /** * Fill a cell data with its left and right paddings so that it is well displayed * @param stringBuilder {@link StringBuilder} we wish to update * @param cell {@link String} data value * @param cellIndex column index * @param columnMaxWidthMapping maximum size for each column * @param textDataType the type of the data (possible types are in {@link TextDataType}) */ private static void fillCell( final StringBuilder stringBuilder, final String cell, final int cellIndex, final Map columnMaxWidthMapping, TextDataType textDataType ) { final int cellPaddingSize = getOptimumCellPadding(cellIndex, cell.length(), columnMaxWidthMapping); // Open the row, if we are on the first cell then append the split symbol. E.g. '|' if (cellIndex == 0) { stringBuilder.append(TABLE_VALUE_SPLIT_SYMBOL); } int rightPadding = PADDING_SIZE; int leftPadding = PADDING_SIZE; if (TextDataType.STRING.equals(textDataType)) { rightPadding = cellPaddingSize; } else if (TextDataType.NUMBER.equals(textDataType)) { leftPadding = cellPaddingSize; } else { throw new IllegalStateException(String.format("Unsupported TextDataType: %s", textDataType.name())); } // Append left padding fillSpace(stringBuilder, leftPadding); // Append the value stringBuilder.append(cell); // Append right padding fillSpace(stringBuilder, rightPadding); // Close the row stringBuilder.append(TABLE_VALUE_SPLIT_SYMBOL); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy