code: /**
   * Copyright 2014-2017 Alexey Ragozin
   *
   * 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 org.gridkit.util.formating;
  
  import java.io.IOException;
  import java.io.StringWriter;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.List;
  import java.util.ListIterator;
  
  /**
   * @author Alexey Ragozin (alexey.ragozin@gmail.com)
   */
  public class TextTable {
  
      public static String formatCsv(TextTable table) {
          StringWriter writer = new StringWriter();
          try {
              table.formatCsv(writer);
          } catch (IOException e) {
              throw new RuntimeException(e);
          }
          return writer.toString();
      }
  
      public static String formatASCII(TextTable table) {
          return table.formatTextTable(Integer.MAX_VALUE);
      }
  
      public static final Comparator<String> NUM_CMP = new NumberCmp();
  
      private List<String[]> rows = new ArrayList<String[]>();
      private int colCount;
  
      public void transpone() {
          int rc = rows.size();
          int cc = colCount;
  
          List<String[]> nrows = new ArrayList<String[]>();
          for(int i = 0; i != cc; ++i) {
              String[] nrow = new String[cc];
              for(int j = 0; j != rc; ++i) {
                  nrow[j] = rows.get(j)[i];
              }
              nrows.add(nrow);
          }
  
          rows = nrows;
          colCount = rc;
      }
  
      public void addRow(String... row) {
          addRow(row, false);
      }
  
      public void addRow(List<String> row) {
          addRow(row.toArray(new String[row.size()]), false);
      }
  
      public void addRow(List<String> row, boolean autoGrow) {
          addRow(row.toArray(new String[row.size()]), autoGrow);
      }
  
      public void addRow(String[] row, boolean autoGrow) {
          if (rows.size() == 0) {
              colCount = row.length;
          }
          if (row.length > colCount) {
              if (autoGrow) {
                  extendRows(row.length);
              }
              else {
                  throw new IllegalArgumentException("Row is longer than table");
              }
          }
          rows.add(Arrays.copyOf(row, colCount));
      }
  
      private void extendRows(int length) {
          for(int i = 0; i != rows.size(); ++i) {
              rows.set(i, Arrays.copyOf(rows.get(i), length));
          }
          colCount = length;
      }
  
      public void addColumnRight(List<String> col) {
          addColumnRight(col.toArray(new String[col.size()]));
      }
  
      public void addColumnRight(String... col) {
          if (col.length > rows.size()) {
              if (rows.size() > 0) {
                  throw new IllegalArgumentException("Column is taller than table");
              } else {
                  for (String c: col) {
                      rows.add(new String[] {c});
                  }
              }
          }
          colCount += 1;
          for(int i = 0; i != rows.size(); ++i) {
              String[] row = rows.get(i);
              row = Arrays.copyOf(row, colCount);
              if (col.length > i) {
                  row[colCount - 1] = col[i];
              }
              rows.set(i, row);
          }
      }
  
      public void addColumnLeft(List<String> col) {
          addColumnLeft(col.toArray(new String[col.size()]));
      }
  
      public void addColumnLeft(String... col) {
          if (col.length > rows.size()) {
              if (rows.size() > 0) {
                  throw new IllegalArgumentException("Column is taller than table");
              } else {
                  for (String c: col) {
                      rows.add(new String[] {c});
                  }
              }
          }
          colCount += 1;
          for(int i = 0; i != rows.size(); ++i) {
              String[] row = rows.get(i);
              String[] nrow = new String[colCount];
              System.arraycopy(row, 0, nrow, 1, row.length);
              if (col.length > i) {
                  nrow[0] = col[i];
              }
              rows.set(i, nrow);
          }
      }
  
      public String[] getRow(int row) {
          return rows.get(row);
      }
  
      public String[] getCol(int col) {
          String[] cc = new String[rows.size()];
          if (col >= 0) {
              for (int i = 0; i != cc.length; ++i) {
                  String[] row = rows.get(i);
                  if (row != null && row.length <= col) {
                      cc[i] = "";
                  } else {
                      cc[i] = row[col];
                  }
              }
          } else {
              Arrays.fill(cc, "");
          }
          return cc;
      }
  
      public void removeColumn(int col) {
          ListIterator<String[]> li = rows.listIterator();
          while(li.hasNext()) {
              String[] row = li.next();
              if (row.length >= col) {
                  String[] nrow = new String[row.length - 1];
                  System.arraycopy(row, 0, nrow, 0, col);
                  System.arraycopy(row, col + 1, nrow, col, nrow.length - col);
                  li.set(nrow);
              }
          }
      }
  
      public int rowCount() {
          return rows.size();
      }
  
      public void sort(int col, boolean exludeHeader, boolean descOrder, Comparator<String> cmp) {
          Comparator<String[]> rowcmp = new RowCmp(col, descOrder, cmp);
          if (exludeHeader) {
              Collections.sort(rows.subList(1, rows.size()), rowcmp);
          }
          else {
              Collections.sort(rows, rowcmp);
          }
      }
  
      public void trim() {
          for (String[] row: rows) {
              for (int i = 0; i != row.length; ++i) {
                  row[i] = row[i].trim();
              }
          }
      }
  
      public void formatCsv(Appendable out) throws IOException {
          for(String[] row: rows) {
              for(int i = 0; i != row.length; ++i) {
                  if (i > 0) {
                      out.append(",");
                  }
                  formatCell(out, row[i]);
              }
              out.append("\n");
          }
      }
  
      private void formatCell(Appendable out, String row) throws IOException {
          if (row == null || row.length() == 0) {
              return;
          }
          if (isCsvSafe(row)) {
              out.append(row);
          }
          else {
              out.append('"');
              for(int i = 0; i != row.length(); ++i) {
                  char c = row.charAt(i);
                  if (c == '"') {
                      out.append("\"\"");
                  }
                  else if (c == '\n') {
                      // replace line end with space
                      out.append(' ');
                  }
                  else {
                      out.append(c);
                  }
              }
              out.append('"');
          }
      }
  
      private boolean isCsvSafe(String row) {
          for(int i = 0; i != row.length(); ++i) {
              char ch = row.charAt(i);
              if (!Character.isJavaIdentifierPart(ch) && "._-".indexOf(ch) < 0) {
                  return false;
              }
          }
          return true;
      }
  
      public String formatTextTable(int maxCellWidth) {
          return formatTable(rows, maxCellWidth, true);
      }
  
      public String formatTextTableUnbordered(int maxCellWidth) {
          return formatTable(rows, maxCellWidth, false);
      }
  
      private String formatTable(List<String[]> content, int maxCell, boolean table) {
          int[] width = new int[content.get(0).length];
          for(String[] row: content) {
              for(int i = 0; i != row.length; ++i) {
                  width[i] = Math.min(Math.max(width[i], measureCell(row[i])), maxCell);
              }
          }
  
          StringBuilder sb = new StringBuilder();
          boolean header = table;
          for(String[] row: content) {
              for(int i = 0; i != width.length; ++i) {
                  renderCell(sb, row[i], width[i]);
                  if (table) {
                      sb.append('|');
                  }
              }
              if (table) {
                  sb.setLength(sb.length() - 1);
              }
              trimTail(sb);
              sb.append('\n');
              if (header) {
                  header = false;
                  for(int n: width) {
                      for(int i = 0; i != n; ++i) {
                          sb.append('-');
                      }
                      sb.append('+');
                  }
                  sb.setLength(sb.length() - 1);
                  trimTail(sb);
                  sb.append('\n');
              }
          }
  
          return sb.toString();
      }
  
      protected int measureCell(String cell) {
          if (cell == null) {
              return 0;
          }
          else {
              int len = 0;
              for(int i = 0; i != cell.length(); ++i) {
                  if (cell.charAt(i) != '\t') {
                      len++;
                  }
              }
              return len;
          }
      }
  
      protected void renderCell(StringBuilder sb, String rawCell, int width) {
          String cell = rawCell == null ? "" : rawCell;
          int tabs = 0;
          for(int i = 0; i != cell.length(); ++i) {
              if (cell.charAt(i) == '\t') {
                  tabs++;
              }
          }
          if (cell.length() - tabs > width) {
              int n = width - 3;
              for(int i = 0; i != cell.length(); ++i) {
                  if (cell.charAt(i) != '\t') {
                      sb.append(cell.charAt(i));
                      if (0 == --n) {
                          break;
                      }
                  }
              }
              sb.append("...");
  
          }
          else {
              if (tabs == 0) {
                  sb.append(cell);
                  for(int s = 0; s != width - cell.length(); ++s) {
                      sb.append(' ');
                  }
              }
              else {
                  int gap = width - cell.length() + tabs;
                  for(int i = 0; i != cell.length(); ++i) {
                      if (cell.charAt(i) == '\t') {
                          int fill = (gap + tabs - 1) / tabs;
                          for(int j = 0; j != fill; ++j) {
                              sb.append(' ');
                          }
                          gap -= fill;
                          tabs--;
                      }
                      else {
                          sb.append(cell.charAt(i));
                      }
                  }
              }
          }
      }
  
      private static void trimTail(StringBuilder sb) {
          while(sb.length() > 0 && sb.charAt(sb.length() - 1) == ' ') {
              sb.setLength(sb.length() - 1);
          }
      }
  
      private static class RowCmp implements Comparator<String[]> {
  
          private final int col;
          private final Comparator<String> cmp;
          private final boolean descOrder;
  
          public RowCmp(int col, boolean descOrder, Comparator<String> cmp) {
              this.col = col;
              this.cmp = cmp;
              this.descOrder = descOrder;
          }
  
          @Override
          public int compare(String[] o1, String[] o2) {
              String s1 = o1[col];
              String s2 = o2[col];
              if (cmp == null) {
                  return (descOrder ? -1 : 1) * s1.compareTo(s2);
              }
              else {
                  return (descOrder ? -1 : 1) * cmp.compare(s1, s2);
              }
          }
      }
  
      private static class NumberCmp implements Comparator<String> {
  
          @Override
          public int compare(String o1, String o2) {
              if (o1.length() == 0 || o2.length() == 0) {
                  return o1.compareTo(o2);
              }
              try {
                  Double v1 = Double.parseDouble(o1);
                  Double v2 = Double.parseDouble(o2);
                  return v1.compareTo(v2);
              }
              catch(NumberFormatException e) {
                  return o1.compareTo(o2);
              }
          }
      }
  }
  