go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/stringutil/table.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package stringutil 9 10 import ( 11 "fmt" 12 "io" 13 "reflect" 14 "strings" 15 "unicode/utf8" 16 ) 17 18 // TableForSlice prints a table for a given slice. 19 // It will infer column names from the struct fields. 20 // If it is a mixed array (i.e. []interface{}) it will probably panic. 21 func TableForSlice[A any](wr io.Writer, collection []A) error { 22 cv := reflect.ValueOf(collection) 23 for cv.Kind() == reflect.Ptr { 24 cv = cv.Elem() 25 } 26 ct := cv.Type() 27 for ct.Kind() == reflect.Ptr || ct.Kind() == reflect.Slice { 28 ct = ct.Elem() 29 } 30 31 columns := make([]string, ct.NumField()) 32 for index := 0; index < ct.NumField(); index++ { 33 columns[index] = ct.Field(index).Name 34 } 35 36 var rows [][]string 37 var rowValue reflect.Value 38 for row := 0; row < cv.Len(); row++ { 39 rowValue = cv.Index(row) 40 rowValues := make([]string, ct.NumField()) 41 for fieldIndex := 0; fieldIndex < ct.NumField(); fieldIndex++ { 42 rowValues[fieldIndex] = fmt.Sprintf("%v", rowValue.Field(fieldIndex).Interface()) 43 } 44 rows = append(rows, rowValues) 45 } 46 47 return Table(wr, columns, rows) 48 } 49 50 // Table writes a table to a given writer. 51 func Table(wr io.Writer, columns []string, rows [][]string) error { 52 if len(columns) == 0 { 53 return nil 54 } 55 write := func(str string) error { 56 _, writeErr := io.WriteString(wr, str) 57 return writeErr 58 } 59 60 /* begin establish max widths of columns */ 61 maxWidths := make([]int, len(columns)) 62 for index, columnName := range columns { 63 maxWidths[index] = stringWidth(columnName) 64 } 65 66 var width int 67 for _, cols := range rows { 68 for index, columnValue := range cols { 69 width = stringWidth(columnValue) 70 if maxWidths[index] < width { 71 maxWidths[index] = width 72 } 73 } 74 } 75 /* end establish max widths of columns */ 76 77 var err error 78 79 /* draw top of column row */ 80 if err = write(tableTopLeft); err != nil { 81 return err 82 } 83 for index := range columns { 84 if err = write(repeat(tableHorizBar, maxWidths[index])); err != nil { 85 return err 86 } 87 if isNotLast(index, columns) { 88 if err = write(tableTopSep); err != nil { 89 return err 90 } 91 } 92 } 93 if err = write(tableTopRight); err != nil { 94 return err 95 } 96 if err = write(newLine); err != nil { 97 return err 98 } 99 /* end draw top of column row */ 100 101 /* draw column names */ 102 if err = write(tableVertBar); err != nil { 103 return err 104 } 105 for index, columnLabel := range columns { 106 if err = write(padRight(columnLabel, maxWidths[index])); err != nil { 107 return err 108 } 109 if isNotLast(index, columns) { 110 if err = write(tableVertBar); err != nil { 111 return err 112 } 113 } 114 } 115 if err = write(tableVertBar); err != nil { 116 return err 117 } 118 if err = write(newLine); err != nil { 119 return err 120 } 121 /* end draw column names */ 122 123 /* draw bottom of column row */ 124 if err = write(tableMidLeft); err != nil { 125 return err 126 } 127 for index := range columns { 128 if err = write(repeat(tableHorizBar, maxWidths[index])); err != nil { 129 return err 130 } 131 if isNotLast(index, columns) { 132 if err = write(tableMidSep); err != nil { 133 return err 134 } 135 } 136 } 137 if err = write(tableMidRight); err != nil { 138 return err 139 } 140 if err = write(newLine); err != nil { 141 return err 142 } 143 /* end draw bottom of column row */ 144 145 /* draw rows */ 146 for _, row := range rows { 147 if err = write(tableVertBar); err != nil { 148 return err 149 } 150 for index, column := range row { 151 if err = write(padRight(column, maxWidths[index])); err != nil { 152 return err 153 } 154 if isNotLast(index, columns) { 155 if err = write(tableVertBar); err != nil { 156 return err 157 } 158 } 159 } 160 if err = write(tableVertBar); err != nil { 161 return err 162 } 163 if err = write(newLine); err != nil { 164 return err 165 } 166 } 167 /* end draw rows */ 168 169 /* draw footer */ 170 if err = write(tableBottomLeft); err != nil { 171 return err 172 } 173 for index := range columns { 174 if err = write(repeat(tableHorizBar, maxWidths[index])); err != nil { 175 return err 176 } 177 if isNotLast(index, columns) { 178 if err = write(tableBottomSep); err != nil { 179 return err 180 } 181 } 182 } 183 if err = write(tableBottomRight); err != nil { 184 return err 185 } 186 if err = write(newLine); err != nil { 187 return err 188 } 189 /* end draw footer */ 190 return nil 191 } 192 193 const ( 194 tableTopLeft = "┌" 195 tableTopRight = "┐" 196 tableBottomLeft = "└" 197 tableBottomRight = "┘" 198 tableMidLeft = "├" 199 tableMidRight = "┤" 200 tableVertBar = "│" 201 tableHorizBar = "─" 202 tableTopSep = "┬" 203 tableBottomSep = "┴" 204 tableMidSep = "┼" 205 newLine = "\n" 206 ) 207 208 func stringWidth(value string) (width int) { 209 var runeWidth int 210 for _, c := range value { 211 runeWidth = utf8.RuneLen(c) 212 if runeWidth > 1 { 213 width += 2 214 } else { 215 width++ 216 } 217 } 218 return 219 } 220 221 func repeat(str string, count int) string { 222 return strings.Repeat(str, count) 223 } 224 225 func padRight(value string, width int) string { 226 valueWidth := stringWidth(value) 227 spaces := width - valueWidth 228 if spaces == 0 { 229 return value 230 } 231 return value + strings.Repeat(" ", spaces) 232 } 233 234 func isNotLast(index int, values []string) bool { 235 return index < (len(values) - 1) 236 }