This is an automated email from the ASF dual-hosted git repository. sunlan pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push: new 9e1f862113 GROOVY-11436: [GINQ] Some non-ascii characters in ascii table can not align correctly in console 9e1f862113 is described below commit 9e1f8621131bfa12db5e579cbaf443308337f24f Author: Daniel Sun <sun...@apache.org> AuthorDate: Sat Jul 6 16:01:39 2024 +0000 GROOVY-11436: [GINQ] Some non-ascii characters in ascii table can not align correctly in console --- .../collection/runtime/AsciiTableMaker.groovy | 205 ++++++++++++--------- .../collection/runtime/AsciiTableMakerTest.groovy | 45 +++++ 2 files changed, 164 insertions(+), 86 deletions(-) diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/AsciiTableMaker.groovy b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/AsciiTableMaker.groovy index cffd3dc6cc..f4e65d6c6f 100644 --- a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/AsciiTableMaker.groovy +++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/AsciiTableMaker.groovy @@ -21,16 +21,15 @@ package org.apache.groovy.ginq.provider.collection.runtime import groovy.transform.CompileStatic import groovy.transform.PackageScope -import static java.util.stream.IntStream.range - /** * @since 4.0.0 */ @PackageScope @CompileStatic class AsciiTableMaker { - private static final int DEFAULT_MAX_WIDTH = Integer.MAX_VALUE private static final String[] EMPTY_STRING_ARRAY = new String[0] + private static final String[][] EMPTY_DIM2_STRING_ARRAY = new String[0][0] + private static final int PADDING = 1 // Padding space for each cell /** * Makes ASCII table for list whose elements are of type {@link NamedRecord}. @@ -42,10 +41,10 @@ class AsciiTableMaker { static <T> String makeAsciiTable(Queryable<T> queryable) { def tableData = queryable.toList() if (tableData) { - List<String[]> list = new ArrayList<>(tableData.size() + 1) def firstRecord = tableData.get(0) if (firstRecord instanceof NamedRecord) { - list.add(((NamedRecord) firstRecord).nameList.toArray(EMPTY_STRING_ARRAY)) + + List<String[]> list = new ArrayList<>(tableData.size()) for (e in tableData) { if (e instanceof NamedRecord) { String[] record = ((NamedRecord) e)*.toString() @@ -53,104 +52,138 @@ class AsciiTableMaker { } } - return '\n' + makeAsciiTable(list, DEFAULT_MAX_WIDTH, true) + String[] headers = ((NamedRecord) firstRecord).nameList.toArray(EMPTY_STRING_ARRAY) + String[][] data = list.toArray(EMPTY_DIM2_STRING_ARRAY) + boolean[] alignLeft = new boolean[headers.length] + Arrays.fill(alignLeft, true) + + return '\n' + buildTable(headers, data, alignLeft) } } return tableData.toString() } - /** - * Create a ascii table - * - * @param table table data - * @param maxWidth Maximum allowed width. Line will be wrapped beyond this width. - * @param leftJustifiedRows If true, it will add "-" as a flag to format string to make it left justified. Otherwise right justified. - * @return the string result representing the ascii table - * @since 4.0.0 - */ - static String makeAsciiTable(List<String[]> table, int maxWidth, boolean leftJustifiedRows) { - StringBuilder result = new StringBuilder(512) - - if (0 == maxWidth) maxWidth = DEFAULT_MAX_WIDTH - - // Create new table array with wrapped rows - List<String[]> tableList = new ArrayList<>(table) - List<String[]> finalTableList = new ArrayList<>(tableList.size() + 1) - for (String[] row : tableList) { - // If any cell data is more than max width, then it will need extra row. - boolean needExtraRow = false - // Count of extra split row. - int splitRow = 0 - do { - needExtraRow = false - String[] newRow = new String[row.length] - for (int i = 0; i < row.length; i++) { - // If data is less than max width, use that as it is. - def col = row[i] ?: '' - if (col.length() < maxWidth) { - newRow[i] = splitRow == 0 ? col : '' - } else if ((col.length() > (splitRow * maxWidth))) { - // If data is more than max width, then crop data at maxwidth. - // Remaining cropped data will be part of next row. - int end = Math.min(col.length(), ((splitRow * maxWidth) + maxWidth)) - newRow[i] = col.substring((splitRow * maxWidth), end) - needExtraRow = true - } else { - newRow[i] = '' - } - } - finalTableList.add(newRow) - if (needExtraRow) { - splitRow++ - } - } while (needExtraRow) + private static String buildTable(String[] headers, String[][] data, boolean[] alignLeft) throws UnsupportedEncodingException { + int[] columnWidths = calculateColumnWidths(headers, data) + + def headerCnt = headers ? headers.length : 0 + def dataElementCnt = countElements(data) + def allElementCnt = headerCnt + dataElementCnt + + headerCnt * 3 // 3 lines of separator + + StringBuilder tableBuilder = new StringBuilder(allElementCnt * 10) + if (headers) { + tableBuilder.append(buildSeparator(columnWidths)) + tableBuilder.append(buildRow(headers, columnWidths, alignLeft)) + } + tableBuilder.append(buildSeparator(columnWidths)) + + for (String[] row : data) { + tableBuilder.append(buildRow(row, columnWidths, alignLeft)) } - String[] firstElem = finalTableList.get(0) - String[][] finalTable = new String[finalTableList.size()][firstElem.length] - for (int i = 0; i < finalTable.length; i++) { - finalTable[i] = finalTableList.get(i) + tableBuilder.append(buildSeparator(columnWidths)) + + return tableBuilder.toString() + } + + private static int countElements(String[][] data) { + if (!data) return 0 + + return data.length * data[0].length + } + + private static int[] calculateColumnWidths(String[] headers, String[][] data) throws UnsupportedEncodingException { + int[] columnWidths = new int[headers.length] + + for (int i = 0; i < headers.length; i++) { + columnWidths[i] = getDisplayWidth(headers[i]) } - // Calculate appropriate Length of each column by looking at width of data in each column. - // Map columnLengths is <column_number, column_length> - Map<Integer, Integer> columnLengths = new HashMap<>() - Arrays.stream(finalTable).forEach(a -> range(0, a.length).forEach(i -> { - columnLengths.putIfAbsent(i, 0) - int len = a[i].length() - if (columnLengths.get(i) < len) { - columnLengths.put(i, len) + for (String[] row : data) { + for (int i = 0; i < row.length; i++) { + int displayWidth = getDisplayWidth(row[i]) + if (displayWidth > columnWidths[i]) { + columnWidths[i] = displayWidth + } } - })) + } - // Prepare format String - def formatString = new StringBuilder(256) - def flag = leftJustifiedRows ? '-' : '' - for (e in columnLengths) { - formatString.append('| %').append(flag).append(e.value).append('s ') + for (int i = 0; i < columnWidths.length; i++) { + columnWidths[i] += PADDING * 2 // Add padding } - formatString.append('|\n') - // Prepare line for top, bottom & below header row - def line = new StringBuilder(256) - for (e in columnLengths) { - line.append('+').append('-' * (e.value + 2)) + return columnWidths + } + + private static String buildRow(String[] row, int[] columnWidths, boolean[] alignLeft) { + StringBuilder rowBuilder = new StringBuilder(Arrays.stream(columnWidths).sum()) + rowBuilder.append("|") + for (int i = 0; i < row.length; i++) { + rowBuilder.append(padString(row[i], columnWidths[i], alignLeft[i])) + rowBuilder.append("|") } - line.append('+\n') + rowBuilder.append("\n") + return rowBuilder.toString() + } - // Print table - result.append(line) - final fmt = formatString.toString() - Arrays.stream(finalTable) - .limit(1) - .forEach(a -> result.append(String.format(fmt, (Object[]) a))) - result.append(line) + private static String buildSeparator(int[] columnWidths) { + StringBuilder separatorBuilder = new StringBuilder(Arrays.stream(columnWidths).sum()) + separatorBuilder.append("+") + for (int width : columnWidths) { + for (int i = 0; i < width; i++) { + separatorBuilder.append("-") + } + separatorBuilder.append("+") + } + separatorBuilder.append("\n") + return separatorBuilder.toString() + } + + private static String padString(String str, int width, boolean alignLeft) { + int displayWidth = getDisplayWidth(str) + StringBuilder padded = new StringBuilder(displayWidth) + int paddingRight = width - displayWidth - PADDING + + if (alignLeft) { + padded.append(" " * PADDING) // Left padding + padded.append(str ?: '') + padded.append(" " * paddingRight) // Right padding + } else { + padded.append(" " * paddingRight) // Left padding + padded.append(str ?: '') + padded.append(" " * PADDING) // Right padding + } + + return padded.toString() + } - range(1, finalTable.length) - .forEach(i -> result.append(String.format(fmt, (Object[]) finalTable[i]))) - result.append(line) + private static int getDisplayWidth(String str) { + if (!str) return 0 + + int width = 0 + for (char c : str.toCharArray()) { + if (isFullWidth(c)) { + width += 2 + } else { + width += 1 + } + } + return width + } - return result.toString() + private static boolean isFullWidth(char c) { + // Unicode block check for full-width characters + Character.UnicodeBlock block = Character.UnicodeBlock.of(c) + return block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS || + block == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS || + block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A || + block == Character.UnicodeBlock.GENERAL_PUNCTUATION || + block == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION || + block == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS || + block == Character.UnicodeBlock.HIRAGANA || + block == Character.UnicodeBlock.KATAKANA || + block == Character.UnicodeBlock.HANGUL_SYLLABLES } private AsciiTableMaker() {} diff --git a/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/provider/collection/runtime/AsciiTableMakerTest.groovy b/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/provider/collection/runtime/AsciiTableMakerTest.groovy new file mode 100644 index 0000000000..b877df0492 --- /dev/null +++ b/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/provider/collection/runtime/AsciiTableMakerTest.groovy @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.groovy.ginq.provider.collection.runtime + +import org.junit.Test + +class AsciiTableMakerTest { + @Test + void makeAsciiTable() { + // GROOVY-11436 + def result = GQ { + from r in [ + [name: 'Daniel', age: 39, location: 'Shanghai'], + [name: '山风小子', age: 40, location: '上海'], + [name: 'Candy', age: 36, location: 'Shanghai'] + ] + select r.name, r.age, r.location + } + def tableStr = AsciiTableMaker.makeAsciiTable(result) + assert tableStr == '\n' + + '+----------+-----+----------+\n' + + '| name | age | location |\n' + + '+----------+-----+----------+\n' + + '| Daniel | 39 | Shanghai |\n' + + '| 山风小子 | 40 | 上海 |\n' + + '| Candy | 36 | Shanghai |\n' + + '+----------+-----+----------+\n' + } +}