Skip to content

Commit

Permalink
Merge pull request #23 from freva/freva/ansi-width
Browse files Browse the repository at this point in the history
Add Styler
  • Loading branch information
freva authored Sep 30, 2022
2 parents f88eab6 + 0cd4887 commit bb98955
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 21 deletions.
45 changes: 33 additions & 12 deletions src/main/java/com/github/freva/asciitable/AsciiTable.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.github.freva.asciitable;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
Expand All @@ -32,7 +35,7 @@ public class AsciiTable {
'╪', '╣', '║', '│', '║', '╟', '─', '┼', '╢', '╠', '═', '╪', '╣', '║', '│', '║', '╚', '═', '╧', '╝'};


static void writeTable(OutputStreamWriter osw, String lineSeparator, Character[] border, Column[] rawColumns, Object[][] data) throws IOException {
static void writeTable(OutputStreamWriter osw, String lineSeparator, Character[] border, Column[] rawColumns, Object[][] data, Styler styler) throws IOException {
if (border.length != NO_BORDERS.length)
throw new IllegalArgumentException("Border characters array must be exactly " + NO_BORDERS.length + " elements long");

Expand All @@ -43,10 +46,10 @@ static void writeTable(OutputStreamWriter osw, String lineSeparator, Character[]
.filter(Column::isVisible)
.toArray(Column[]::new);

writeTable(osw, lineSeparator, border, columns, stringData);
writeTable(osw, lineSeparator, border, columns, stringData, styler);
}

private static void writeTable(OutputStreamWriter osw, String lineSeparator, Character[] border, Column[] columns, String[][] data) throws IOException {
private static void writeTable(OutputStreamWriter osw, String lineSeparator, Character[] border, Column[] columns, String[][] data, Styler styler) throws IOException {
int[] colWidths = getColWidths(columns, data);
OverflowBehaviour[] overflows = Arrays.stream(columns).map(Column::getOverflowBehaviour).toArray(OverflowBehaviour[]::new);
boolean insertNewline = writeLine(osw, colWidths, border[0], border[1], border[2], border[3]);
Expand All @@ -55,15 +58,18 @@ private static void writeTable(OutputStreamWriter osw, String lineSeparator, Cha
HorizontalAlign[] aligns = Arrays.stream(columns).map(Column::getHeaderAlign).toArray(HorizontalAlign[]::new);
String[] header = Arrays.stream(columns).map(Column::getHeader).toArray(String[]::new);
if (insertNewline) osw.write(lineSeparator);
writeData(osw, colWidths, overflows, aligns, header, border[4], border[5], border[6], lineSeparator);
writeData(osw, colWidths, overflows, aligns, header, border[4], border[5], border[6], lineSeparator,
styler == null ? null : (col, rows) -> styler.styleHeader(columns[col], col, rows));
osw.write(lineSeparator);
insertNewline = writeLine(osw, colWidths, border[7], border[8], border[9], border[10]);
}

HorizontalAlign[] dataAligns = Arrays.stream(columns).map(Column::getDataAlign).toArray(HorizontalAlign[]::new);
for (int i = 0; i < data.length; i++) {
if (insertNewline) osw.write(lineSeparator);
writeData(osw, colWidths, overflows, dataAligns, data[i], border[11], border[12], border[13], lineSeparator);
int row = i;
writeData(osw, colWidths, overflows, dataAligns, data[i], border[11], border[12], border[13], lineSeparator,
styler == null ? null : (col, rows) -> styler.styleCell(columns[col], row, col, rows));
if (i < data.length - 1) {
osw.write(lineSeparator);
insertNewline = writeLine(osw, colWidths, border[14], border[15], border[16], border[17]);
Expand All @@ -76,7 +82,8 @@ private static void writeTable(OutputStreamWriter osw, String lineSeparator, Cha
String[] footer = Arrays.stream(columns).map(Column::getFooter).toArray(String[]::new);
insertNewline = writeLine(osw, colWidths, border[18], border[19], border[20], border[21]);
if (insertNewline) osw.write(lineSeparator);
writeData(osw, colWidths, overflows, aligns, footer, border[22], border[23], border[24], lineSeparator);
writeData(osw, colWidths, overflows, aligns, footer, border[22], border[23], border[24], lineSeparator,
styler == null ? null : (col, rows) -> styler.styleFooter(columns[col], col, rows));
}

if (border[26] != null) osw.write(lineSeparator);
Expand All @@ -103,7 +110,8 @@ private static boolean writeLine(OutputStreamWriter osw, int[] colWidths, Charac
*/
@SuppressWarnings("deprecated")
private static void writeData(OutputStreamWriter osw, int[] colWidths, OverflowBehaviour[] overflows, HorizontalAlign[] horizontalAligns,
String[] contents, Character left, Character columnSeparator, Character right, String lineSeparator) throws IOException {
String[] contents, Character left, Character columnSeparator, Character right, String lineSeparator,
BiFunction<Integer, List<String>, List<String>> styler) throws IOException {
List<List<String>> linesContents = IntStream.range(0, colWidths.length)
.mapToObj(i -> {
String text = i < contents.length && contents[i] != null ? contents[i] : "";
Expand All @@ -130,11 +138,19 @@ private static void writeData(OutputStreamWriter osw, int[] colWidths, OverflowB
.mapToInt(List::size)
.max().orElse(0);

List<List<String>> justifiedLinesContents = styler == null ? null : IntStream.range(0, colWidths.length)
.mapToObj(col -> styler.apply(col, IntStream.range(0, numLines)
.mapToObj(i -> justify(i < linesContents.get(col).size() ? linesContents.get(col).get(i) : "", horizontalAligns[col], colWidths[col], PADDING))
.collect(Collectors.toList())))
.collect(Collectors.toList());

for (int line = 0; line < numLines; line++) {
if (left != null) osw.append(left);
for (int col = 0; col < colWidths.length; col++) {
String item = linesContents.get(col).size() <= line ? "" : linesContents.get(col).get(line);
writeJustified(osw, item, horizontalAligns[col], colWidths[col], PADDING);
if (justifiedLinesContents == null) {
String item = linesContents.get(col).size() <= line ? "" : linesContents.get(col).get(line);
writeJustified(osw, item, horizontalAligns[col], colWidths[col], PADDING);
} else osw.write(justifiedLinesContents.get(col).get(line));
if (columnSeparator != null && col != colWidths.length - 1) osw.write(columnSeparator);
}
if (right != null) osw.append(right);
Expand Down Expand Up @@ -171,9 +187,14 @@ private static int getNumColumns(Column[] columns, Object[][] data) {
.reduce(columns.length, Math::max);
}

/** Returns the width of each line in resulting table not counting the line break character(s) */
private static int getTableWidth(int[] colWidths) {
return Arrays.stream(colWidths).sum() + PADDING * (colWidths.length + 1) - 1;
static String justify(String str, HorizontalAlign align, int length, int minPadding) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(length);
try (OutputStreamWriter osw = new OutputStreamWriter(baos)) {
writeJustified(osw, str, align, length, minPadding);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return baos.toString();
}

/**
Expand Down
14 changes: 11 additions & 3 deletions src/main/java/com/github/freva/asciitable/AsciiTableBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,34 @@
import java.io.UncheckedIOException;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;

public class AsciiTableBuilder {

private String lineSeparator = System.lineSeparator();
private Character[] border = AsciiTable.BASIC_ASCII;
private Styler styler;
private String[] header;
private String[] footer;
private Column[] columns;
private Object[][] data;

/** Set the line separator to use between table rows. Default is {@link System#lineSeparator()}. */
public AsciiTableBuilder lineSeparator(String lineSeparator) {
this.lineSeparator = lineSeparator;
this.lineSeparator = Objects.requireNonNull(lineSeparator, "line separator cannot be null");
return this;
}

/** Set the table border style. Default is {@link AsciiTable#BASIC_ASCII}. */
public AsciiTableBuilder border(Character[] border) {
this.border = border;
this.border = Objects.requireNonNull(border, "border cannot be null");
return this;
}

/** Set the table styler, default is noop */
public AsciiTableBuilder styler(Styler styler) {
this.styler = Objects.requireNonNull(styler, "styler cannot be null");
return this;
}

Expand Down Expand Up @@ -93,7 +101,7 @@ public void writeTo(OutputStream os) {

try {
OutputStreamWriter osw = new OutputStreamWriter(os);
AsciiTable.writeTable(osw, lineSeparator, border, columns, data);
AsciiTable.writeTable(osw, lineSeparator, border, columns, data, styler);
osw.flush();
} catch (IOException e) {
throw new UncheckedIOException(e);
Expand Down
56 changes: 56 additions & 0 deletions src/main/java/com/github/freva/asciitable/Styler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.github.freva.asciitable;

import java.util.List;

/**
* Allows styling the table by adding zero-width characters (e.g. ANSI escape codes)
* to the content. This interface will be invoked just before the data is written,
* the inputs are therefore already split into the lines and the text is justified.
*
* WARNING: If any of the methods add any non-zero-width characters or remove any characters,
* the table will be misaligned.
*/
public interface Styler {

/**
* Style the cell value, e.g. by adding ANSI escape codes.
*
* @param column Column of the cell
* @param row row number of the cell (0-indexed)
* @param col column number of the cell (0-indexed)
* @param data List of strings (lines) in this cell. Guaranteed to be at least 1 element. Unless the original
* cell data was longer than the max column width AND text overflow behavior is NEWLINE, this will
* contain exactly 1 element.
* @return List of strings (lines) in this cell.
*/
default List<String> styleCell(Column column, int row, int col, List<String> data) {
return data;
}

/**
* Style the header value, e.g. by adding ANSI escape codes.
*
* @param column Column of the header
* @param col column number of the cell (0-indexed)
* @param data {@link Column#getHeader()}, but split into lines to satisfy {@link Column#getMaxWidth()} and
* {@link Column#getOverflowBehaviour()}, and justified per {@link Column#headerAlign(HorizontalAlign)}
* @return List of strings (lines) in this header cell.
*/
default List<String> styleHeader(Column column, int col, List<String> data) {
return data;
}

/**
* Style the footer value, e.g. by adding ANSI escape codes.
*
* @param column Column of the footer
* @param col column number of the cell (0-indexed)
* @param data {@link Column#getFooter()} ()}, but split into lines to satisfy {@link Column#getMaxWidth()} and
* {@link Column#getOverflowBehaviour()}, and justified per {@link Column#footerAlign(HorizontalAlign)}
* @return List of strings (lines) in this footer cell.
*/
default List<String> styleFooter(Column column, int col, List<String> data) {
return data;
}

}
54 changes: 48 additions & 6 deletions src/test/java/com/github/freva/asciitable/AsciiTableTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.time.Instant;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

import static com.github.freva.asciitable.HorizontalAlign.*;
import static org.junit.Assert.assertEquals;
Expand Down Expand Up @@ -603,6 +603,51 @@ public void calculatesCorrectColumnWidthWithLineBreakInHeaderAndFooter() {
assertEquals(expected, actual);
}

@Test
public void styler() {
String[] headers = {"First", "Second", "Third"};
String[][] data = {{"11", "12", "13"}, {"21", "22", "23"}, {"31", "32", "33"}};

final String RESET = "\u001B[m";
final String RED = "\u001B[31m";
final String GREEN = "\u001B[32m";
final String YELLOW = "\u001B[33m";
final String BLUE = "\u001B[34m";
Styler styler = new Styler() {
@Override
public List<String> styleCell(Column column, int row, int col, List<String> data) {
String color = row % 2 == 0 ? (col % 2 == 0 ? RED : BLUE) : (col % 2 == 0 ? BLUE : RED);
return data.stream().map(line -> color + line + RESET).collect(Collectors.toList());
}

@Override
public List<String> styleHeader(Column column, int col, List<String> data) {
String color = col % 2 == 0 ? GREEN : YELLOW;
return data.stream().map(line -> color + line + RESET).collect(Collectors.toList());
}

@Override
public List<String> styleFooter(Column column, int col, List<String> data) {
String color = col % 2 == 0 ? YELLOW : GREEN;
return data.stream().map(line -> color + line + RESET).collect(Collectors.toList());
}
};

String actual = AsciiTable.builder().styler(styler).header(headers).footer(headers).data(data).asString();
assertEquals(String.format(
"+-------+--------+-------+%1$s" +
"|%2$s First %6$s|%3$s Second %6$s|%2$s Third %6$s|%1$s" +
"+-------+--------+-------+%1$s" +
"|%5$s 11 %6$s|%4$s 12 %6$s|%5$s 13 %6$s|%1$s" +
"+-------+--------+-------+\n" +
"|%4$s 21 %6$s|%5$s 22 %6$s|%4$s 23 %6$s|%1$s" +
"+-------+--------+-------+%1$s" +
"|%5$s 31 %6$s|%4$s 32 %6$s|%5$s 33 %6$s|%1$s" +
"+-------+--------+-------+%1$s" +
"|%3$s First %6$s|%2$s Second %6$s|%3$s Third %6$s|%1$s" +
"+-------+--------+-------+", System.lineSeparator(), GREEN, YELLOW, BLUE, RED, RESET), actual);
}

@Test(expected = IllegalArgumentException.class)
public void validateTooFewBorderChars() {
String[] headers = {"Lorem", "Ipsum", "Dolor", "Sit"};
Expand Down Expand Up @@ -641,11 +686,8 @@ private static void assertParagraphs(OverflowBehaviour overflowBehaviour, String
}

private static void assertJustify(String expected, String str, HorizontalAlign align, int length, int minPadding) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(baos);
AsciiTable.writeJustified(osw, str, align, length, minPadding);
osw.flush();
assertEquals(expected, baos.toString());
String actual = AsciiTable.justify(str, align, length, minPadding);
assertEquals(expected, actual);
}

private static class Planet {
Expand Down

0 comments on commit bb98955

Please sign in to comment.