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'
+    }
+}

Reply via email to