tabular formatter: new option 'separateBy' to print horizontal separator on each change of given column v_0 tip
authorFrantišek Kučera <franta-hg@frantovo.cz>
Sun, 04 Feb 2024 16:10:37 +0100
branchv_0
changeset 255099bb96f8d8d
parent 254 c4b901ff0703
tabular formatter: new option 'separateBy' to print horizontal separator on each change of given column
java/sql-dk/src/main/java/info/globalcode/sql/dk/configuration/Properties.java
java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/TabularFormatter.java
     1.1 --- a/java/sql-dk/src/main/java/info/globalcode/sql/dk/configuration/Properties.java	Fri Sep 23 18:05:50 2022 +0200
     1.2 +++ b/java/sql-dk/src/main/java/info/globalcode/sql/dk/configuration/Properties.java	Sun Feb 04 16:10:37 2024 +0100
     1.3 @@ -20,15 +20,16 @@
     1.4  import javax.xml.bind.annotation.XmlTransient;
     1.5  import static info.globalcode.sql.dk.Functions.findByName;
     1.6  import java.util.Collections;
     1.7 +import java.util.regex.Pattern;
     1.8  
     1.9  /**
    1.10   * <p>
    1.11   * List of configurables.</p>
    1.12   *
    1.13   * <p>
    1.14 - * Can be backed by defaults – if value for given name is nof found in this instance, we will
    1.15 - * look into defaults. Methods also accept defaultValue parameter – is used if property is nof found
    1.16 - * even in default properties.</p>
    1.17 + * Can be backed by defaults – if value for given name is nof found in this
    1.18 + * instance, we will look into defaults. Methods also accept defaultValue
    1.19 + * parameter – is used if property is nof found even in default properties.</p>
    1.20   *
    1.21   * <p>
    1.22   * Typical use: </p>
    1.23 @@ -94,6 +95,11 @@
    1.24  		return p == null ? defaultValue : Integer.valueOf(p.getValue());
    1.25  	}
    1.26  
    1.27 +	public Pattern getPattern(String name, Pattern defaultValue) {
    1.28 +		Property p = findProperty(name);
    1.29 +		return p == null ? defaultValue : Pattern.compile(p.getValue());
    1.30 +	}
    1.31 +
    1.32  	public boolean hasProperty(String name) {
    1.33  		return findByName(this, name) != null;
    1.34  	}
     2.1 --- a/java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/TabularFormatter.java	Fri Sep 23 18:05:50 2022 +0200
     2.2 +++ b/java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/TabularFormatter.java	Sun Feb 04 16:10:37 2024 +0100
     2.3 @@ -27,9 +27,12 @@
     2.4  import static info.globalcode.sql.dk.formatting.CommonProperties.COLORFUL_DESCRIPTION;
     2.5  import java.sql.SQLException;
     2.6  import java.sql.SQLXML;
     2.7 +import java.util.ArrayList;
     2.8  import java.util.List;
     2.9 +import java.util.Objects;
    2.10  import java.util.logging.Level;
    2.11  import java.util.logging.Logger;
    2.12 +import java.util.regex.Pattern;
    2.13  
    2.14  /**
    2.15   * <p>
    2.16 @@ -49,6 +52,7 @@
    2.17  @PropertyDeclaration(name = TabularFormatter.PROPERTY_ASCII, defaultValue = "false", type = Boolean.class, description = "whether to use ASCII table borders instead of unicode ones")
    2.18  @PropertyDeclaration(name = TabularFormatter.PROPERTY_TRIM, defaultValue = "false", type = Boolean.class, description = "whether to trim the values to fit the column width")
    2.19  @PropertyDeclaration(name = TabularFormatter.PROPERTY_HEADER_TYPE, defaultValue = "true", type = Boolean.class, description = "whether to print data types in column headers")
    2.20 +@PropertyDeclaration(name = TabularFormatter.PROPERTY_SEPARATE_BY, defaultValue = "", type = String.class, description = "colum(s) whose change triggers separator printing")
    2.21  public class TabularFormatter extends AbstractFormatter {
    2.22  
    2.23  	private static final Logger log = Logger.getLogger(TabularFormatter.class.getName());
    2.24 @@ -58,6 +62,7 @@
    2.25  	public static final String PROPERTY_ASCII = "ascii";
    2.26  	public static final String PROPERTY_TRIM = "trim";
    2.27  	public static final String PROPERTY_HEADER_TYPE = "headerTypes";
    2.28 +	public static final String PROPERTY_SEPARATE_BY = "separateBy";
    2.29  	protected ColorfulPrintWriter out;
    2.30  	private boolean firstResult = true;
    2.31  	private int[] columnWidth;
    2.32 @@ -73,6 +78,27 @@
    2.33  	 * Print data type of each column in the header
    2.34  	 */
    2.35  	private final boolean printHeaderTypes;
    2.36 +	/**
    2.37 +	 * When values of columns (names specified by this pattern) changes
    2.38 +	 * between two consecutive rows, horizontal separator is printed.
    2.39 +	 */
    2.40 +	private final Pattern separateBy;
    2.41 +	/**
    2.42 +	 * Indexes of columns that matches {@linkplain #separateBy}
    2.43 +	 */
    2.44 +	private final List<Boolean> separators = new ArrayList<>();
    2.45 +	/**
    2.46 +	 * Internal counter for buffered data.
    2.47 +	 */
    2.48 +	private int currentColumnsCount = 0;
    2.49 +	/**
    2.50 +	 * Buffered data to be compared and printed later.
    2.51 +	 */
    2.52 +	private final List<Object> currentRow = new ArrayList<>();
    2.53 +	/**
    2.54 +	 * Buffered data to be compared later.
    2.55 +	 */
    2.56 +	private final List<Object> previousRow = new ArrayList<>();
    2.57  
    2.58  	public TabularFormatter(FormatterContext formatterContext) {
    2.59  		super(formatterContext);
    2.60 @@ -81,6 +107,7 @@
    2.61  		trimValues = formatterContext.getProperties().getBoolean(PROPERTY_TRIM, false);
    2.62  		printHeaderTypes = formatterContext.getProperties().getBoolean(PROPERTY_HEADER_TYPE, true);
    2.63  		out.setColorful(formatterContext.getProperties().getBoolean(COLORFUL, true));
    2.64 +		separateBy = formatterContext.getProperties().getPattern(PROPERTY_SEPARATE_BY, null);
    2.65  	}
    2.66  
    2.67  	@Override
    2.68 @@ -89,12 +116,21 @@
    2.69  		printResultSeparator();
    2.70  
    2.71  		initColumnWidths(header.getColumnCount());
    2.72 +		currentRow.clear();
    2.73 +		previousRow.clear();
    2.74 +		separators.clear();
    2.75  
    2.76  		printTableIndent();
    2.77  		printTableBorder("╭");
    2.78  
    2.79  		List<ColumnDescriptor> columnDescriptors = header.getColumnDescriptors();
    2.80  
    2.81 +		if (separateBy != null) {
    2.82 +			for (ColumnDescriptor cd : columnDescriptors) {
    2.83 +				separators.add(separateBy.matcher(cd.getLabel()).matches());
    2.84 +			}
    2.85 +		}
    2.86 +
    2.87  		for (ColumnDescriptor cd : columnDescriptors) {
    2.88  			// padding: make header cell at least same width as data cells in this column
    2.89  			int typeWidth = printHeaderTypes ? cd.getTypeName().length() + HEADER_TYPE_PREFIX.length() + HEADER_TYPE_SUFFIX.length() : 0;
    2.90 @@ -160,11 +196,49 @@
    2.91  
    2.92  	@Override
    2.93  	public void writeColumnValue(Object value) {
    2.94 -		super.writeColumnValue(value);
    2.95 +		if (separateBy == null) super.writeColumnValue(value);
    2.96  		writeColumnValueInternal(value);
    2.97  	}
    2.98  
    2.99  	protected void writeColumnValueInternal(Object value) {
   2.100 +		if (separateBy == null) {
   2.101 +			printColumnValue(value);
   2.102 +		} else {
   2.103 +			currentRow.add(value);
   2.104 +			currentColumnsCount++;
   2.105 +			int columnsCount = getCurrentColumnsHeader().getColumnCount();
   2.106 +			if (currentColumnsCount % columnsCount == 0) {
   2.107 +				if (!previousRow.isEmpty()) {
   2.108 +					boolean hasChanges = false;
   2.109 +					for (int i = 0; i < previousRow.size(); i++) {
   2.110 +						Object previous = previousRow.get(i);
   2.111 +						Object current = currentRow.get(i);
   2.112 +						boolean isSepating = separators.get(i);
   2.113 +						if (isSepating && !Objects.equals(previous, current)) {
   2.114 +							hasChanges = true;
   2.115 +							break;
   2.116 +						}
   2.117 +					}
   2.118 +
   2.119 +					if (hasChanges) {
   2.120 +						printRecordSeparator();
   2.121 +					}
   2.122 +				}
   2.123 +
   2.124 +				for (Object o : currentRow) {
   2.125 +					super.writeColumnValue(value);
   2.126 +					printColumnValue(o);
   2.127 +				}
   2.128 +
   2.129 +				previousRow.clear();
   2.130 +				previousRow.addAll(currentRow);
   2.131 +				currentRow.clear();
   2.132 +				currentColumnsCount = 0;
   2.133 +			}
   2.134 +		}
   2.135 +	}
   2.136 +
   2.137 +	protected void printColumnValue(Object value) {
   2.138  
   2.139  		if (isCurrentColumnFirst()) {
   2.140  			printTableIndent();
   2.141 @@ -181,6 +255,20 @@
   2.142  
   2.143  	}
   2.144  
   2.145 +	protected void printRecordSeparator() {
   2.146 +		int columnCount = getCurrentColumnsHeader().getColumnCount();
   2.147 +		printTableIndent();
   2.148 +		printTableBorder("├");
   2.149 +		for (int i = 1; i <= columnCount; i++) {
   2.150 +			if (i > 1) {
   2.151 +				printTableBorder("┼");
   2.152 +			}
   2.153 +			printTableBorder(repeat('─', getColumnWidth(i) + 2));
   2.154 +		}
   2.155 +		printTableBorder("┤");
   2.156 +		out.println();
   2.157 +	}
   2.158 +
   2.159  	protected void printValueWithWhitespaceReplaced(String text) {
   2.160  		Functions.printValueWithWhitespaceReplaced(out, text, TerminalColor.Cyan, TerminalColor.Red);
   2.161  	}
   2.162 @@ -226,6 +314,12 @@
   2.163  	}
   2.164  
   2.165  	@Override
   2.166 +	public void writeStartRow() {
   2.167 +		super.writeStartRow();
   2.168 +		currentColumnsCount = 0;
   2.169 +	}
   2.170 +
   2.171 +	@Override
   2.172  	public void writeEndRow() {
   2.173  		super.writeEndRow();
   2.174  		writeEndRowInternal();