package de.am_soft.sm_mtg.view.report.text.data; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.TreeMap; import java.util.TreeSet; import java.util.function.BiFunction; import java.util.stream.Collectors; import de.vandermeer.asciitable.AT_Cell; import de.vandermeer.asciitable.AT_CellContext; import de.vandermeer.asciitable.AT_ColumnWidthCalculator; import de.vandermeer.asciitable.AT_Row; import de.vandermeer.skb.interfaces.document.TableRowType; /** * Custom column width caluclator working around some limitations of spanning columns. *

* We prefer rendering tables using {@code CWC_LongestLine}, but that has some limitations in case * of spanning columns: In the original implementation, the width of spanning columns itself are * compared to the width of the same non-spanning columns to see which one is larger. The problem * with this approach is that the spanning columns get the space of the columns they span as well * during rendering and all the space of the spanning and spanned columns might be enough to render * the content. But the space of the spanning column itself might be larger than the one of non- * spanning columns, which leads to non-spanning columns might get more space than their longest * line really deserves. *

*

* Consider two rows, with the first being some header spanning all columns of the second row and * the second row containing actual tabular data. The header might be pretty long compared to the * individual columns of the second row, which e.g. might only provide short numbers of some kind. * The text for the header needs to be the last column of the first row, because that's how spans * work. If that text is e.g. 50 chars long, but the last column of the second row otherwise only 10 * chars, the last column gets the width of 50 even if e.g. 10 columns with 10 chars each exist in * the second row. Those 10 * 10 chars would easily be enough for 50 chars of the header if that * column would span all other columns, but nevertheless its width is taken on its own. *

*

* So this calculator has been implemented working around those limitations by taking the length of * all spanned columns into account as well when deciding the width of the one spanning column. The * other possible workaround would be to keep using {@code CWC_LongestLine} and set a maximum width * for columns which would otherwise be calculated too long. The problem with that is that one needs * to know the exact width of all those columns, which might be different for different content e.g. * because of differently localized texts etc. Calculating the width properly in the first place is * the better approach. *

*

* This CWC doesn't support minimum and maximum cell lengths currently, because we simply didn't * need those yet. *

*@see Spanning Sizes Last Column Too Wide */ class VwrDvResultsCwc implements AT_ColumnWidthCalculator { /** * The width of one individual cell. *

* A cell either has a width not, because it's {@code null} and this way by convention consumed * by some other non-{@code null} cell spanning all individual {@code null}-cell on its left. *

*/ private static class CellWidth { /** * Singleton for all cases in which no width is available because cells are spanned by other * cells. */ private static final CellWidth NONE = new CellWidth(null); /** * The width of the cell or {@code null} if none is available. */ private final Integer width; /** * CTOR simply storing the given arg. * * @param width The width or {@code null}. */ private CellWidth(Integer width) { this.width = width; } /** * Check if the associated cell has some width. * * @return {@code true} if some width is available, {@code false} otherwise. */ private boolean hasWidth() { return this.width != null; } /** * Calculate the width of the given cell. * * @param cell to calculate the width for, not {@code null}. * @return The width of the cell, which might be {@link #NONE}. */ private static CellWidth of(AT_Cell cell) { cell = Objects.requireNonNull(cell, "No cell given."); Object content = cell.getContent(); AT_CellContext context = cell.getContext(); if (content == null) { return CellWidth.NONE; } String[] lines = new VwrDvResultsCwcStringifier().apply(content); int retVal = 0; // We follow the approach of CWC_LongestLine and take each individual line of a cell // into account. for (String line : lines) { int padding = context.getPaddingLeft() + context.getPaddingRight(); int lineWidth = line.length() + padding; retVal = Math.max(retVal, lineWidth); } return new CellWidth(retVal); } /** * Remapper if cell widths are stored in some map and need to be replaced using {@code merge}. */ private static class Remapper implements BiFunction { @Override public CellWidth apply(CellWidth oldVal, CellWidth newVal) { oldVal = Objects.requireNonNull(oldVal, "No old width given."); newVal = Objects.requireNonNull(newVal, "No new width given."); boolean retNew = Integer.compare(oldVal.width, newVal.width) < 0; return retNew ? newVal : oldVal; } } } /** * Width of all cells in one row. *

* This should be used with content-rows only, we don't need to care about other rows, and it * doesn't filter things like spanning or spanned cells yet. It's really all widths for all * cells, though cells might have no width at all here because they get spanned. That piece of * information is later used to handle those cases specially. *

*/ @SuppressWarnings("serial") private static class CellWidths extends ArrayList { /** * Calc the width for all individual cells in the given row, regardless if they are spanned * at some point. So some cells might not have a real width in the end. * * @param row to calculate all cell-widths for, not {@code null}. * @return Widths of all individual cells. */ private static CellWidths of(AT_Row row) { return Objects.requireNonNull(row, "No row given.") .getCells() .stream() .map((cell) -> CellWidth.of(cell)) .collect(Collectors.toCollection(CellWidths::new)); } } /** * All individual widths of all cells in all rows. *

* All widths of all cells in all rows are needed to decide at some point if the space occupied * by spanning columns fits into the combined space of the spanned columns or if the spanning * columns define maximum column width instead. One needs to know the widht of all individual * columns, which cols don't have a width because they are spanned etc. *

*/ @SuppressWarnings("serial") private static class CellWidthsAll extends ArrayList { /** * Calc all individual widths of all cells in all rows. * * @param rows to calculate the widths of cells for. * @return Widths of all cells. */ private static CellWidthsAll of(ContentRows rows) { return Objects.requireNonNull(rows, "No rows given.") .stream() .map((row) -> CellWidths.of(row)) .collect(Collectors.toCollection(CellWidthsAll::new)); } } /** * Widths of all non-spanning cells only! *

* To be able to calculate the width of spanning cells, one needs the underlying non-spanning * ones and this class manages those by their index. When processing spanning cells, this way * one can easily retrieve the widths of the spanned cells this way, combine them and check if * the resulting width is enough for the spanning column or not. *

*/ @SuppressWarnings("serial") private static class CellWidthsNonSpan extends HashMap { /** * Calculate the widths of non-spanning cells. * * @param all cell widths of all rows. * @return Widths of non-spanning cells only. */ private static CellWidthsNonSpan of(CellWidthsAll all) { CellWidthsNonSpan retVal = new CellWidthsNonSpan(); for (CellWidths row : all) { for (int cell = 0; cell < row.size(); ++cell) { // All cells with no width before them are spanning ones and need to be ignored. if ((cell > 0) && !row.get(cell - 1).hasWidth()) { continue; } CellWidth width = row.get(cell); if (!width.hasWidth()) { continue; } retVal.merge(cell, width, new CellWidth.Remapper()); } } return retVal; } } /** * Widths of all spanning cells only! *

* To decide if spanning cells fit into the width of their spanned cells, one needs the width of * the spanning cells themself as well of course and this class provides those. *

*/ @SuppressWarnings("serial") private static class CellWidthsSpan extends HashMap { /** * Calculate the widths of spanning cells. * * @param all cell widths of all rows. * @return Widths of spanning cells only. */ private static CellWidthsSpan of(CellWidthsAll all) { CellWidthsSpan retVal = new CellWidthsSpan(); for (CellWidths row : all) { for (int cell = 1; cell < row.size(); ++cell) { // All cells with a width before them are non-spanning ones. if (row.get(cell - 1).hasWidth()) { continue; } // Spanned cells can be ignored as well, they don't have any width on their own. CellWidth width = row.get(cell); if (!width.hasWidth()) { continue; } retVal.merge(cell, width, new CellWidth.Remapper()); } } return retVal; } } /** * Widths of all spanning cells NOT fitting into their spanned cells *

* This class provides all those spanning cells, which don't fit into the space provided by * their spanned cells, so that the spanning cells provide the width of their associated content * cell instead. *

*/ @SuppressWarnings("serial") private static class CellWidthsSpanIf extends HashMap { /** * Calculate the widths of spanning cells. * * @param all cell widths of all rows. * @return Widths of spanning cells only. */ private static CellWidthsSpanIf of( CellWidthsAll all, CellWidthsNonSpan nonSpan) { CellSpans spans = CellSpans.of( all); CellWidthsSpan spanWidths = CellWidthsSpan.of(all); CellWidthsSpanIf retVal = new CellWidthsSpanIf(); for (CellSpan span : spans) { int nonSpanWidth = span .stream() .map((col) -> nonSpan.get(col)) .mapToInt((width) -> width.width) .sum(); int spanIdx = span.last(); CellWidth spanWidth = spanWidths.get(spanIdx); if (spanWidth.width > nonSpanWidth) { retVal.put(spanIdx, spanWidth); } } return retVal; } } /** * Resulting widths of all cells. *

* This class provides the resulting widths of all cells, containing spanning and non-spanning * ones as necessary, with the spanning ones possibly overwriting non-spanning. *

*/ @SuppressWarnings("serial") private static class CellWidthsResult extends TreeMap { /** * Combine widths of spanning and non-spanning cells, witht he former overwriting the latter * if necessary. * * @param nonSpan Non-spanning cells. * @param spanIf Spanning cells IF their content doesn't fit the spanned cells. * @param expected number of columns in the result. * @return Widths of all cells. */ private static CellWidthsResult of( CellWidthsNonSpan nonSpan, CellWidthsSpanIf spanIf, int expected) { CellWidthsResult retVal = new CellWidthsResult(); retVal.putAll(nonSpan); retVal.putAll(spanIf); if (retVal.size() != expected) { throw new UnsupportedOperationException(String.format ( "Too few widths. Expected: %d; Calculated: %s", expected, retVal )); } return retVal; } } /** * Content rows only. */ @SuppressWarnings("serial") private static class ContentRows extends ArrayList { /** * Get all content rows from the given rows. * * @param rows to filter content rows from. * @return Content rows. */ private static ContentRows of(List rows) { return Objects.requireNonNull(rows, "No rows given.") .stream() .filter((row) -> row.getType() == TableRowType.CONTENT) .collect(Collectors.toCollection(ContentRows::new)); } } /** * One logical cell spanning multiple others. *

* This class takes the indexes of all the cells part of one spanning cell in one row. Because * by convention the rightmost index is the only cell with the content, an ordered structure of * indexes is used, so that one can simply access the highest index and get the content-cell. *

*/ @SuppressWarnings("serial") private static class CellSpan extends TreeSet { } /** * Multiple spanning cells, not necessarily within one and the same row. *

* Different cells in different rows can span differently and all of those spanning cells are * necessary to check if their content fits into the space provided by the non-spanning cells * the spanning ones span. One doesn't need to care in which row those spanning cells are placed * at that point, we only need to know all of those. *

*/ @SuppressWarnings("serial") private static class CellSpans extends ArrayList { /** * Filter all spanning columns from the given cell widths. * * @param all cell widths of all rows. * @return All spanning cells only. */ private static CellSpans of(CellWidthsAll all) { CellSpans retVal = new CellSpans(); for (CellWidths row : all) { CellSpan cells = new CellSpan(); for (int cell = 0; cell < row.size(); ++cell) { // If the former cell is not a spanned one, we don't need to care further. if (!cells.contains(cell - 1)) { continue; } // This might be a spanned cell, in which case we remember and proceed. if (!row.get(cell).hasWidth()) { cells.add(cell); continue; } // The is the content-cell actual spanning others, so store all associated ones // and proceed, because the same row might contain additional spanning cells. cells.add(cell); retVal.add(cells); cells = new CellSpan(); } if (!cells.isEmpty()) { throw new UnsupportedOperationException(String.format ( "Spanned cells without content-cell left: %s", cells )); } } return retVal; } } @Override public int[] calculateColumnWidths( LinkedList allRows, int colCnt, int tableWidth) { ContentRows contRows = ContentRows.of(allRows); CellWidthsAll all = CellWidthsAll.of(contRows); CellWidthsNonSpan nonSpan = CellWidthsNonSpan.of(all); CellWidthsSpanIf spanIf = CellWidthsSpanIf.of(all, nonSpan); CellWidthsResult result = CellWidthsResult.of(nonSpan, spanIf, colCnt); int[] retVal = new int[colCnt]; result.forEach((k, v) -> retVal[k] = v.width); return retVal; } }