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  }