From e5f8bb0e2e84e5d63b933cd5fa489d575ab5005f Mon Sep 17 00:00:00 2001 From: David Gamba Date: Tue, 12 Apr 2022 21:48:48 -0600 Subject: [PATCH] clitable: add support for emojis --- clitable/clitable.go | 7 ++++- clitable/clitable_test.go | 26 ++++++++++++++++++- clitable/go.mod | 2 ++ clitable/go.sum | 2 ++ clitable/tableinfo.go | 52 ++++++++++++++++++++++++++++++-------- clitable/tableinfo_test.go | 11 ++++++++ 6 files changed, 87 insertions(+), 13 deletions(-) diff --git a/clitable/clitable.go b/clitable/clitable.go index 6837efc..2edd83c 100644 --- a/clitable/clitable.go +++ b/clitable/clitable.go @@ -236,7 +236,12 @@ func (tp *TablePrinter) fprint(w io.Writer, t Table, tableInfo *TableInfo) error } multiLine := strings.Split(row.Fields[j], "\n") if len(multiLine) > i { - fmt.Printf(" %-"+strconv.Itoa(tableInfo.ColumnWidths[j])+"s ", multiLine[i]) + _, c := StringWidth(multiLine[i]) + if c > 0 { + fmt.Printf(" %-"+strconv.Itoa(tableInfo.ColumnWidths[j]-c)+"s ", multiLine[i]) + } else { + fmt.Printf(" %-"+strconv.Itoa(tableInfo.ColumnWidths[j])+"s ", multiLine[i]) + } } else { fmt.Printf(" %-"+strconv.Itoa(tableInfo.ColumnWidths[j])+"s ", " ") } diff --git a/clitable/clitable_test.go b/clitable/clitable_test.go index a9458c4..dba4f14 100644 --- a/clitable/clitable_test.go +++ b/clitable/clitable_test.go @@ -40,7 +40,7 @@ func TestTableStructData(t *testing.T) { data := &Data{[]struct { Name string ID int - }{{"Hello", 1}, {"World", 2}}} + }{{"Hello", 1}, {"World ⚽⛪⚽⛪Å®", 2}}} clitable.NewTablePrinter().Fprint(os.Stdout, data) simpleData := [][]string{{"Hello", "1"}, {"World", "2"}} @@ -54,3 +54,27 @@ func TestTableStructData(t *testing.T) { clitable.NewTablePrinter().SetStyle(clitable.Ascii).Print(data) clitable.NewTablePrinter().SetStyle(clitable.Space).Print(data) } + +func TestStringWidth(t *testing.T) { + tests := []struct { + name string + input string + lenght int + count int + }{ + {"A", "A", 1, 0}, + {"Å", "Å", 1, 0}, + {"⚽", "⚽", 2, 1}, + {"⛪", "⛪", 2, 1}, + {"®", "®", 1, 0}, + {"World Å®⚽⛪⚽⛪", "World Å®⚽⛪⚽⛪", 16, 4}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + l, c := clitable.StringWidth(test.input) + if test.lenght != l || test.count != c { + t.Errorf("expected: %d, got: %d, %s - %#U\n", test.lenght, l, test.input, []byte(test.input)) + } + }) + } +} diff --git a/clitable/go.mod b/clitable/go.mod index 364a70e..947c9c1 100644 --- a/clitable/go.mod +++ b/clitable/go.mod @@ -3,3 +3,5 @@ module github.com/DavidGamba/dgtools/clitable go 1.18 require github.com/DavidGamba/go-getoptions v0.25.3 + +require golang.org/x/text v0.3.7 // indirect diff --git a/clitable/go.sum b/clitable/go.sum index fcdac6f..ff0892c 100644 --- a/clitable/go.sum +++ b/clitable/go.sum @@ -1,2 +1,4 @@ github.com/DavidGamba/go-getoptions v0.25.3 h1:lSPcMkwWvVZU05C+Uz4DKnKN5wz4bcD1QvJ/QHCRexo= github.com/DavidGamba/go-getoptions v0.25.3/go.mod h1:qLaLSYeQ8sUVOfKuu5JT5qKKS3OCwyhkYSJnoG+ggmo= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/clitable/tableinfo.go b/clitable/tableinfo.go index 473d803..f9a83c4 100644 --- a/clitable/tableinfo.go +++ b/clitable/tableinfo.go @@ -11,6 +11,10 @@ package clitable import ( "fmt" "strings" + "unicode" + "unicode/utf8" + + "golang.org/x/text/unicode/norm" ) // TableInfo - Table information @@ -29,6 +33,36 @@ func (i *TableInfo) String() string { return str } +var single_width_table = []*unicode.RangeTable{ + unicode.Letter, + unicode.Digit, + unicode.Latin, + {R16: []unicode.Range16{ + {0x0080, 0x00FF, 1}, // latin-1 supplement + }}, +} + +func StringWidth(s string) (width int, doubleWidthCount int) { + normalized := string(norm.NFC.Bytes([]byte(s))) + l := len(normalized) // string len check, will count 2 or 3 for a single emoji + u := utf8.RuneCountInString(normalized) // rune count, will correctly count number of chars + diff := l - u // diff + c := 0 // track multibyte chars that should have a 2 char width + if diff > 0 { + for _, r := range normalized { + // fmt.Printf("l: %d, u: %d, %#U\n", l, u, r) + if unicode.IsOneOf(single_width_table, r) { + continue + } + rl := utf8.RuneLen(r) + if rl > 1 { // multibyte char + c++ + } + } + } + return u + c, c +} + // GetTableInfo - Iterates over all the elements of the table to get number of Colums, Colum widths, etc. func GetTableInfo(t Table) (*TableInfo, error) { var rows int @@ -52,19 +86,15 @@ func GetTableInfo(t Table) (*TableInfo, error) { for i, cData := range row.Fields { // Some records might be multiline, split the record and get the biggest width multiLine := strings.Split(cData, "\n") - if len(multiLine) > 1 { - l := 0 - for _, d := range multiLine { - if len(d) > l { - l = len(d) - } + ll := 0 + for _, d := range multiLine { + l, _ := StringWidth(d) + if l > ll { + ll = l } - rowColumnWidths[i] = l - rowRows[i] = len(multiLine) - } else { - rowColumnWidths[i] = len(cData) - rowRows[i] = 1 } + rowColumnWidths[i] = ll + rowRows[i] = len(multiLine) } perRowColumnWidths = append(perRowColumnWidths, rowColumnWidths) perRowRows = append(perRowRows, rowRows) diff --git a/clitable/tableinfo_test.go b/clitable/tableinfo_test.go index 3b5000a..540f432 100644 --- a/clitable/tableinfo_test.go +++ b/clitable/tableinfo_test.go @@ -38,6 +38,17 @@ func Test_GetTableInfo(t *testing.T) { RowHeights: []int{1}, }, false}, + {"single column, single row emoji ⚽⛪Å®", + args{bytes.NewBufferString("hello ⚽⛪Å®")}, + &clitable.TableInfo{ + Columns: 1, + Rows: 1, + PerRowColumnWidths: [][]int{{12}}, + PerRowRows: [][]int{{1}}, + ColumnWidths: []int{12}, + RowHeights: []int{1}, + }, + false}, {"multi column, single row", args{bytes.NewBufferString("a,bb")}, &clitable.TableInfo{