Coverage Summary for Class: AsciiTable (it.polimi.ingsw.gc14.View.TUI)

Class Class, % Method, % Branch, % Line, %
AsciiTable 100% (1/1) 90.9% (10/11) 52.6% (40/76) 82.2% (60/73)


 package it.polimi.ingsw.gc14.View.TUI;
 import java.util.*;
 
 /**
  * General-purpose helper for building fixed-width ASCII/Unicode tables in a TUI.
  *
  * <p>A table is built by adding rows one at a time, then calling {@link #build()}
  * to obtain the fully rendered string. Column width is computed automatically
  * from the widest cell across all rows.
  *
  * <p>Cell widths are measured in terminal columns, not Java {@code char} units:
  * wide characters (emoji, CJK) count as 2 columns each.
  *
  * <p>Example usage:
  * <pre>{@code
  * AsciiTable table = new AsciiTable(BorderStyle.UNICODE, 3);
  * table.addHeader("Name", "🍖", "⭐");
  * table.addRow("Alice", "4", "12");
  * table.addRow("Bob",   "2", "8");
  * System.out.println(table.build());
  * }</pre>
  */
 public class AsciiTable {
     /** Border style used to draw corners, lines, and junctions. */
     private final BorderStyle s;
 
     /** Number of columns in this table. */
     private final int cols;
 
     /** All rows of the table, in insertion order. */
     private final List<List<String>> rows = new ArrayList<>();
 
     /** Indices of rows after which a horizontal separator line is drawn. */
     private final List<Integer> separators = new ArrayList<>();
 
     /**
      * Creates a new empty table with the given border style and column count.
      *
      * @param s    the border style to use when rendering.
      * @param cols the number of columns.
      */
     public AsciiTable(BorderStyle s, int cols) {
         this.s = s; this.cols = cols;
     }
 
     /**
      * Appends a row using varargs cells.
      *
      * @param cells one value per column.
      */
     public void addRow(String... cells) { rows.add(Arrays.asList(cells)); }
 
     /**
      * Appends a row from an existing list.
      *
      * @param cells one value per column.
      */
     public void addRow(List<String> cells) { rows.add(cells); }
 
     /**
      * Inserts a header row at position 0 and marks it with a separator line below it.
      *
      * @param cells one header label per column.
      */
     public void addHeader(String... cells) {
         rows.add(0, Arrays.asList(cells));
         separators.add(0);
     }
 
     /**
      * Marks a separator line to be drawn after the last row added so far.
      */
     public void addSeparator() { separators.add(rows.size() - 1); }
 
     /**
      * Renders the table to a multi-line string.
      * Column width is the widest cell in display columns (wide chars = 2), plus 1.
      *
      * @return the fully rendered table as a multi-line string.
      */
     public String build() {
         var sb = new StringBuilder();
         int maxWidth = rows.stream()
                 .mapToInt(x -> x.stream().mapToInt(AsciiTable::displayWidth).max().orElse(0))
                 .max().orElse(0) + 1;
         sb.append(hline(s.tl(), s.mt(), s.tr(), maxWidth)).append('\n');
 
         for (int i = 0; i < rows.size(); i++) {
             sb.append(s.v());
             for (String cell : rows.get(i))
                 sb.append(rpad(" " + cell, maxWidth)).append(s.v());
             sb.append('\n');
             if (separators.contains(i) && i < rows.size() - 1)
                 sb.append(hline(s.sl(), s.sx(), s.sr(), maxWidth)).append('\n');
         }
         sb.append(hline(s.bl(), s.mb(), s.br(), maxWidth));
         return sb.toString();
     }
     /**
      * Builds a single horizontal borderline across all columns.
      *
      * @param l        left-end character.
      * @param m        middle junction character (between columns).
      * @param r        right-end character.
      * @param maxWidth width in display columns of each cell (including padding).
      * @return the rendered horizontal line string.
      */
     private String hline(String l, String m, String r, int maxWidth) {
         var sb = new StringBuilder(l);
         for (int i = 0; i < cols; i++) {
             sb.repeat(Objects.requireNonNull(s.h()), maxWidth);
             if (i < cols - 1) sb.append(m);
         }
         return sb.append(r).toString();
     }
 
     /**
      * Pads {@code s} with trailing spaces so its display width equals {@code w}.
      * If the string is already at or over {@code w} columns, it is returned as-is.
      */
     private static String rpad(String s, int w) {
         int dw = displayWidth(s);
         if (dw >= w) return s;
         return s + " ".repeat(w - dw);
     }
 
     /**
      * Places two pre-rendered text blocks side by side, separated by a gap.
      * Left-block lines are padded to a uniform display width so the right block
      * always starts at the same column. Uses {@link #displayWidth} for measurement.
      *
      * @param left  lines of the left block.
      * @param right lines of the right block.
      * @param gap   number of space characters between the two blocks.
      * @return the combined multi-line string.
      */
     public static String sideBySide(List<String> left, List<String> right, int gap) {
         int leftWidth = left.stream().mapToInt(AsciiTable::displayWidth).max().orElse(0);
         int maxHeight = Math.max(left.size(), right.size());
         String padding = " ".repeat(gap);
 
         var sb = new StringBuilder();
         for (int i = 0; i < maxHeight; i++) {
             String l = i < left.size() ? left.get(i) : " ".repeat(leftWidth);
             l = rpad(l, leftWidth);
             String r = i < right.size() ? right.get(i) : "";
             sb.append(l).append(padding).append(r).append('\n');
         }
         return sb.toString();
     }
 
 
     /**
      * Returns the number of terminal columns required to display {@code s}.
      * Wide characters (emoji, CJK, full-width) count as 2; all others as 1.
      *
      * @param s the string to measure; ANSI escape sequences are stripped before counting.
      * @return the display width in terminal columns.
      */
     public static int displayWidth(String s) {
         s = s.replaceAll("\033\\[[^m]*m", "");
         int w = 0;
         for (int i = 0; i < s.length(); ) {
             int cp = s.codePointAt(i);
             w += isWide(cp) ? 2 : 1;
             i += Character.charCount(cp);
         }
         return w;
     }
 
     /**
      * Returns {@code true} when {@code cp} is a wide (2-column) character.
      * Covers CJK blocks, Hangul, full-width forms, and emoji (including
      * the specific emoji used in this project: 🍖 U+1F357, ⭐ U+2B50).
      */
     private static boolean isWide(int cp) {
         if (cp < 0x1100) return false;
         if (cp <= 0x115F) return true;
         if (cp < 0x2E80) {
             return cp == 0x2B50
                 || cp == 0x2B55;
         }
         if (cp <= 0x303E) return true;
         if (cp < 0x3041) return false;
         if (cp <= 0xA4CF) return true;
         if (cp < 0xA960) return false;
         if (cp <= 0xA97F) return true;
         if (cp < 0xAC00) return false;
         if (cp <= 0xD7AF) return true;
         if (cp < 0xF900) return false;
         if (cp <= 0xFAFF) return true;
         if (cp < 0xFE10) return false;
         if (cp <= 0xFE1F) return true;
         if (cp < 0xFE30) return false;
         if (cp <= 0xFE6F) return true;
         if (cp < 0xFF00) return false;
         if (cp <= 0xFF60) return true;
         if (cp < 0xFFE0) return false;
         if (cp <= 0xFFE6) return true;
         if (cp < 0x1F004) return false;
         if (cp <= 0x1FAFF) return true;
 
         if (cp < 0x20000) return false;
         return cp <= 0x3FFFD;
     }
 }