package com.github.ompc.greys.core.textui;
import com.github.ompc.greys.core.util.GaStringUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import static java.lang.Math.abs;
import static java.lang.Math.max;
import static java.lang.String.format;
import static org.apache.commons.lang3.StringUtils.*;
/**
* 表格组件
* Created by oldmanpushcart@gmail.com on 15/5/7.
*/
public class TTable implements TComponent {
// 各个列的定义
private final ColumnDefine[] columnDefineArray;
// 边框
private final Border border = new Border();
// 内边距
private int padding;
public TTable(ColumnDefine[] columnDefineArray) {
this.columnDefineArray = null == columnDefineArray
? new ColumnDefine[0]
: columnDefineArray;
}
public TTable(int columnNum) {
this.columnDefineArray = new ColumnDefine[columnNum];
for (int index = 0; index < this.columnDefineArray.length; index++) {
columnDefineArray[index] = new ColumnDefine();
}
}
@Override
public String rendering() {
final StringBuilder tableSB = new StringBuilder();
// process width cache
final int[] widthCacheArray = new int[getColumnCount()];
for (int index = 0; index < widthCacheArray.length; index++) {
widthCacheArray[index] = abs(columnDefineArray[index].getWidth());
}
final int rowCount = getRowCount();
for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) {
final boolean isFirstRow = rowIndex == 0;
final boolean isLastRow = rowIndex == rowCount - 1;
// 打印首分隔行
if (isFirstRow
&& border.has(Border.BORDER_OUTER_TOP)) {
tableSB.append(drawSeparationLine(widthCacheArray)).append("\n");
}
// 打印内部分割行
if (!isFirstRow
&& border.has(Border.BORDER_INNER_H)) {
tableSB.append(drawSeparationLine(widthCacheArray)).append("\n");
}
// 绘一行
tableSB.append(drawRow(widthCacheArray, rowIndex));
// 打印结尾分隔行
if (isLastRow
&& border.has(Border.BORDER_OUTER_BOTTOM)) {
tableSB.append(drawSeparationLine(widthCacheArray)).append("\n");
}
}
return tableSB.toString();
}
private String drawRow(int[] widthCacheArray, int rowIndex) {
final StringBuilder rowSB = new StringBuilder();
final Scanner[] scannerArray = new Scanner[getColumnCount()];
try {
boolean hasNextLine;
do {
hasNextLine = false;
final StringBuilder segmentSB = new StringBuilder();
for (int colIndex = 0; colIndex < getColumnCount(); colIndex++) {
final int width = widthCacheArray[colIndex];
final boolean isFirstColOfRow = colIndex == 0;
final boolean isLastColOfRow = colIndex == widthCacheArray.length - 1;
final String borderChar;
if (isFirstColOfRow
&& border.has(Border.BORDER_OUTER_LEFT)) {
borderChar = "|";
} else if (!isFirstColOfRow
&& border.has(Border.BORDER_INNER_V)) {
borderChar = "|";
} else {
borderChar = EMPTY;
}
if (null == scannerArray[colIndex]) {
scannerArray[colIndex] = new Scanner(
new StringReader(GaStringUtils.wrap(getData(rowIndex, columnDefineArray[colIndex]), width)));
}
final Scanner scanner = scannerArray[colIndex];
final String data;
if (scanner.hasNextLine()) {
data = scanner.nextLine();
hasNextLine = true;
} else {
data = EMPTY;
}
if (width > 0) {
final ColumnDefine columnDefine = columnDefineArray[colIndex];
final String dataFormat = getDataFormat(columnDefine, width, data);
final String paddingChar = repeat(" ", padding);
segmentSB.append(format(borderChar + paddingChar + dataFormat + paddingChar, data));
}
if (isLastColOfRow) {
if (border.has(Border.BORDER_OUTER_RIGHT)) {
segmentSB.append("|");
}
segmentSB.append("\n");
}
}
if (hasNextLine) {
rowSB.append(segmentSB);
}
} while (hasNextLine);
return rowSB.toString();
} finally {
for (Scanner scanner : scannerArray) {
if (null != scanner) {
scanner.close();
}
}
}
}
private String getData(int rowIndex, ColumnDefine columnDefine) {
return columnDefine.getRowCount() <= rowIndex
? EMPTY
: columnDefine.rows.get(rowIndex);
}
private String getDataFormat(ColumnDefine columnDefine, int width, String data) {
switch (columnDefine.align) {
case MIDDLE: {
final int length = StringUtils.length(data);
final int diff = width - length;
final int left = diff / 2;
return repeat(" ", diff - left) + "%s" + repeat(" ", left);
}
case RIGHT: {
return "%" + width + "s";
}
case LEFT:
default: {
return "%-" + width + "s";
}
}
}
/*
* 获取表格行数
*/
private int getRowCount() {
int rowCount = 0;
for (ColumnDefine columnDefine : columnDefineArray) {
rowCount = max(rowCount, columnDefine.getRowCount());
}
return rowCount;
}
/*
* 定位最后一个列
*/
private int indexLastCol(final int[] widthCacheArray) {
for (int colIndex = widthCacheArray.length - 1; colIndex >= 0; colIndex--) {
final int width = widthCacheArray[colIndex];
if (width <= 0) {
continue;
}
return colIndex;
}
return 0;
}
/*
* 打印分隔行
*/
private String drawSeparationLine(final int[] widthCacheArray) {
final StringBuilder separationLineSB = new StringBuilder();
final int lastCol = indexLastCol(widthCacheArray);
final int colCount = widthCacheArray.length;
for (int colIndex = 0; colIndex < colCount; colIndex++) {
final int width = widthCacheArray[colIndex];
if (width <= 0) {
continue;
}
final boolean isFirstCol = colIndex == 0;
final boolean isLastCol = colIndex == lastCol;
if (isFirstCol
&& border.has(Border.BORDER_OUTER_LEFT)) {
separationLineSB.append("+");
}
if (!isFirstCol
&& border.has(Border.BORDER_INNER_V)) {
separationLineSB.append("+");
}
separationLineSB.append(repeat("-", width + 2 * padding));
if (isLastCol
&& border.has(Border.BORDER_OUTER_RIGHT)) {
separationLineSB.append("+");
}
}
return separationLineSB.toString();
}
/**
* 添加数据行
*
* @param columnDataArray 数据数组
*/
public TTable addRow(Object... columnDataArray) {
if (null != columnDataArray) {
for (int index = 0; index < columnDefineArray.length; index++) {
final ColumnDefine columnDefine = columnDefineArray[index];
if (index < columnDataArray.length
&& null != columnDataArray[index]) {
columnDefine.rows.add(replaceTab(columnDataArray[index].toString()));
} else {
columnDefine.rows.add(EMPTY);
}
}
}
return this;
}
/**
* 对齐方向
*/
public enum Align {
/**
* 左对齐
*/
LEFT,
/**
* 右对齐
*/
RIGHT,
/**
* 居中对齐
*/
MIDDLE
}
/**
* 列定义
*/
public static class ColumnDefine {
// 列宽度
private final int width;
// 是否自动宽度
private final boolean isAutoResize;
// 对齐方式
private final Align align;
// 数据行集合
private final List<String> rows = new ArrayList<String>();
public ColumnDefine(int width, boolean isAutoResize, Align align) {
this.width = width;
this.isAutoResize = isAutoResize;
this.align = align;
}
public ColumnDefine(Align align) {
this(0, true, align);
}
public ColumnDefine(int width) {
this(width, false, Align.LEFT);
}
public ColumnDefine(int width, Align align) {
this(width, false, align);
}
public ColumnDefine() {
this(Align.LEFT);
}
/**
* 获取当前列的宽度
*
* @return 宽度
*/
public int getWidth() {
// 如果是固定宽度,则直接返回预设定的宽度
if (!isAutoResize) {
return width;
}
// 如果是自动扩展宽度,则需要根据计算当前列的所有字符串最大可视宽度
int maxWidth = 0;
for (String data : rows) {
maxWidth = max(width(data), maxWidth);
}
return maxWidth;
}
/**
* 获取当前列的行数
*
* @return 当前列的行数
*/
public int getRowCount() {
return rows.size();
}
}
/**
* 设置内边距大小
*
* @param padding 内边距
*/
public TTable padding(int padding) {
this.padding = padding;
return this;
}
/**
* 获取表格列总数
*
* @return 表格列总数
*/
public int getColumnCount() {
return columnDefineArray.length;
}
/**
* 替换TAB制表符<br/>
* 替换为4个空格
*
* @param string 原始字符串
* @return 替换后的字符串
*/
private static String replaceTab(String string) {
return StringUtils.replace(string, "\t", " ");
}
/**
* 获取一个字符串的可视宽度<br/>
* 什么叫一个字符串的可视宽度呢?很简单,因为字符串有换行行为,所以一个字符串的宽度不能简单的根据字符串的长度来判断<br/>
* 例如:"abc\n1234",这个字符串的可视宽度为4
*
* @param string 字符串
* @return 字符串可视宽度
*/
private static int width(String string) {
int maxWidth = 0;
final Scanner scanner = new Scanner(new StringReader(string));
try {
while (scanner.hasNextLine()) {
maxWidth = max(length(scanner.nextLine()), maxWidth);
}
} finally {
scanner.close();
}
return maxWidth;
}
/**
* 获取表格边框设置
*
* @return 表格边框
*/
public Border getBorder() {
return border;
}
/**
* 边框样式设置
*/
public class Border {
private int borders = BORDER_OUTER | BORDER_INNER;
/**
* 外部上边框
*/
public static final int BORDER_OUTER_TOP = 1 << 0;
/**
* 外部右边框
*/
public static final int BORDER_OUTER_RIGHT = 1 << 1;
/**
* 外部下边框
*/
public static final int BORDER_OUTER_BOTTOM = 1 << 2;
/**
* 外部左边框
*/
public static final int BORDER_OUTER_LEFT = 1 << 3;
/**
* 内边框:水平
*/
public static final int BORDER_INNER_H = 1 << 4;
/**
* 内边框:垂直
*/
public static final int BORDER_INNER_V = 1 << 5;
/**
* 外边框
*/
public static final int BORDER_OUTER = BORDER_OUTER_TOP | BORDER_OUTER_BOTTOM | BORDER_OUTER_LEFT | BORDER_OUTER_RIGHT;
/**
* 内边框
*/
public static final int BORDER_INNER = BORDER_INNER_H | BORDER_INNER_V;
/**
* 无边框
*/
public static final int BORDER_NON = 0;
/**
* 是否包含指定边框类型<br/>
* 只要当前边框策略命中其中之一即认为命中
*
* @param borderArray 目标边框数组
* @return 当前边框策略是否拥有指定的边框
*/
public boolean has(int... borderArray) {
if (null == borderArray) {
return false;
}
for (int b : borderArray) {
if ((this.borders & b) == b) {
return true;
}
}
return false;
}
/**
* 获取表格边框设置
*
* @return 边框位
*/
public int get() {
return borders;
}
/**
* 设置表格边框
*
* @param border 边框位
* @return this
*/
public Border set(int border) {
this.borders = border;
return this;
}
public Border add(int border) {
return set(get() | border);
}
public Border remove(int border) {
return set(get() ^ border);
}
}
}