pkg.re/essentialkaos/ek.10@v12.41.0+incompatible/fmtutil/table/table.go (about)

     1  // Package table contains methods and structs for rendering data in tabular format
     2  package table
     3  
     4  // ////////////////////////////////////////////////////////////////////////////////// //
     5  //                                                                                    //
     6  //                         Copyright (c) 2022 ESSENTIAL KAOS                          //
     7  //      Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0>     //
     8  //                                                                                    //
     9  // ////////////////////////////////////////////////////////////////////////////////// //
    10  
    11  import (
    12  	"fmt"
    13  	"strings"
    14  
    15  	"pkg.re/essentialkaos/ek.v12/fmtc"
    16  	"pkg.re/essentialkaos/ek.v12/mathutil"
    17  	"pkg.re/essentialkaos/ek.v12/strutil"
    18  	"pkg.re/essentialkaos/ek.v12/terminal/window"
    19  )
    20  
    21  // ////////////////////////////////////////////////////////////////////////////////// //
    22  
    23  // Column alignment flags
    24  const (
    25  	ALIGN_LEFT   uint8 = 0
    26  	ALIGN_CENTER       = 1
    27  	ALIGN_RIGHT        = 2
    28  )
    29  
    30  // ////////////////////////////////////////////////////////////////////////////////// //
    31  
    32  // Table is struct which can be used for table rendering
    33  type Table struct {
    34  	Sizes     []int    // Custom columns sizes
    35  	Headers   []string // Slice with headers
    36  	Alignment []uint8  // Columns alignment
    37  
    38  	// Slice with data
    39  	data [][]string
    40  
    41  	// Separator cache
    42  	separator string
    43  
    44  	// Flag will be set if header is rendered
    45  	headerShown bool
    46  
    47  	// Slice with auto calculated sizes
    48  	columnSizes []int
    49  }
    50  
    51  // ////////////////////////////////////////////////////////////////////////////////// //
    52  
    53  // HeaderCapitalize is flag for capitalizing headers by default
    54  var HeaderCapitalize = false
    55  
    56  // HeaderColorTag is fmtc tag used for headers by default for all tables
    57  var HeaderColorTag = "{*}"
    58  
    59  // SeparatorSymbol used for separator generation
    60  var SeparatorSymbol = "-"
    61  
    62  // ColumnSeparatorSymbol is column separator symbol
    63  var ColumnSeparatorSymbol = "|"
    64  
    65  // MaxWidth is a maximum table width
    66  var MaxWidth = 0
    67  
    68  // ////////////////////////////////////////////////////////////////////////////////// //
    69  
    70  // NewTable creates new table struct
    71  func NewTable(headers ...string) *Table {
    72  	return &Table{Headers: headers}
    73  }
    74  
    75  // ////////////////////////////////////////////////////////////////////////////////// //
    76  
    77  // SetHeaders sets headers
    78  func (t *Table) SetHeaders(headers ...string) *Table {
    79  	if t == nil {
    80  		return nil
    81  	}
    82  
    83  	t.Headers = headers
    84  
    85  	return t
    86  }
    87  
    88  // SetSizes sets size of columns
    89  func (t *Table) SetSizes(sizes ...int) *Table {
    90  	if t == nil {
    91  		return nil
    92  	}
    93  
    94  	t.Sizes = sizes
    95  
    96  	return t
    97  }
    98  
    99  // SetAlignments sets aligment of columns
   100  func (t *Table) SetAlignments(align ...uint8) *Table {
   101  	if t == nil {
   102  		return nil
   103  	}
   104  
   105  	t.Alignment = align
   106  
   107  	return t
   108  }
   109  
   110  // Add adds given data to stack
   111  func (t *Table) Add(data ...interface{}) *Table {
   112  	if t == nil {
   113  		return nil
   114  	}
   115  
   116  	if len(data) == 0 {
   117  		return t
   118  	}
   119  
   120  	t.data = append(t.data, convertSlice(data))
   121  
   122  	return t
   123  }
   124  
   125  // Print renders given data
   126  func (t *Table) Print(data ...interface{}) *Table {
   127  	if t == nil {
   128  		return nil
   129  	}
   130  
   131  	if len(data) == 0 {
   132  		return t
   133  	}
   134  
   135  	if len(t.Headers) == 0 && len(t.Sizes) == 0 {
   136  		setColumnsSizes(t, len(data))
   137  	}
   138  
   139  	prepareRender(t)
   140  	renderRowData(t, convertSlice(data), len(t.columnSizes))
   141  
   142  	return t
   143  }
   144  
   145  // HasData returns true if table stack has some data
   146  func (t *Table) HasData() bool {
   147  	return t != nil && len(t.data) != 0
   148  }
   149  
   150  // Separator renders separator
   151  func (t *Table) Separator() *Table {
   152  	if t == nil {
   153  		return nil
   154  	}
   155  
   156  	if t.separator == "" {
   157  		t.separator = strings.Repeat(SeparatorSymbol, getSeparatorSize(t))
   158  	}
   159  
   160  	fmtc.Println("{s}" + t.separator + "{!}")
   161  
   162  	return t
   163  }
   164  
   165  // RenderHeaders renders headers
   166  func (t *Table) RenderHeaders() {
   167  	if len(t.columnSizes) == 0 {
   168  		calculateColumnSizes(t)
   169  	}
   170  
   171  	renderHeaders(t)
   172  }
   173  
   174  // Render renders data
   175  func (t *Table) Render() *Table {
   176  	if t == nil {
   177  		return nil
   178  	}
   179  
   180  	// Nothing to render
   181  	if len(t.Headers) == 0 && len(t.data) == 0 {
   182  		return t
   183  	}
   184  
   185  	prepareRender(t)
   186  
   187  	if len(t.Headers) == 0 {
   188  		t.Separator()
   189  	}
   190  
   191  	if t.data != nil {
   192  		renderData(t)
   193  	}
   194  
   195  	// Remove data after rendering
   196  	t.separator = ""
   197  	t.data = nil
   198  	t.columnSizes = nil
   199  	t.headerShown = false
   200  
   201  	return t
   202  }
   203  
   204  // ////////////////////////////////////////////////////////////////////////////////// //
   205  
   206  // prepareRender prepare table for render
   207  func prepareRender(t *Table) {
   208  	if len(t.columnSizes) == 0 {
   209  		calculateColumnSizes(t)
   210  	}
   211  
   212  	if !t.headerShown {
   213  		renderHeaders(t)
   214  	}
   215  }
   216  
   217  // renderHeaders render headers
   218  func renderHeaders(t *Table) {
   219  	t.headerShown = true
   220  
   221  	if len(t.Headers) == 0 {
   222  		return
   223  	}
   224  
   225  	t.Separator()
   226  
   227  	totalHeaders := len(t.Headers)
   228  	totalColumns := len(t.columnSizes)
   229  
   230  	var headerText string
   231  
   232  	for columnIndex, columnSize := range t.columnSizes {
   233  		if columnIndex >= totalHeaders {
   234  			headerText = strings.Repeat(" ", columnSize)
   235  		} else {
   236  			headerText = t.Headers[columnIndex]
   237  		}
   238  
   239  		if HeaderCapitalize {
   240  			headerText = strings.ToUpper(headerText)
   241  		}
   242  
   243  		fmtc.Printf(" " + HeaderColorTag + formatText(headerText, t.columnSizes[columnIndex], getAlignment(t, columnIndex)) + "{!} ")
   244  
   245  		if columnIndex+1 != totalColumns {
   246  			fmtc.Printf("{s}%s{!}", ColumnSeparatorSymbol)
   247  		} else {
   248  			fmtc.NewLine()
   249  		}
   250  	}
   251  
   252  	t.Separator()
   253  }
   254  
   255  // renderData render table data
   256  func renderData(t *Table) {
   257  	totalColumns := len(t.columnSizes)
   258  
   259  	for _, rowData := range t.data {
   260  		renderRowData(t, rowData, totalColumns)
   261  	}
   262  
   263  	t.Separator()
   264  }
   265  
   266  // renderRowData render data in row
   267  func renderRowData(t *Table, rowData []string, totalColumns int) {
   268  	for columnIndex, columnData := range rowData {
   269  		if columnIndex == totalColumns {
   270  			break
   271  		}
   272  
   273  		if strutil.Len(fmtc.Clean(columnData)) > t.columnSizes[columnIndex] {
   274  			fmtc.Print(" " + strutil.Ellipsis(columnData, t.columnSizes[columnIndex]) + " ")
   275  		} else {
   276  			if columnIndex+1 == totalColumns && getAlignment(t, columnIndex) == ALIGN_LEFT {
   277  				fmtc.Print(" " + formatText(columnData, -1, ALIGN_LEFT))
   278  			} else {
   279  				fmtc.Print(" " + formatText(columnData, t.columnSizes[columnIndex], getAlignment(t, columnIndex)) + " ")
   280  			}
   281  		}
   282  
   283  		if columnIndex+1 != totalColumns {
   284  			fmtc.Printf("{s}%s{!}", ColumnSeparatorSymbol)
   285  		}
   286  	}
   287  
   288  	fmtc.NewLine()
   289  }
   290  
   291  // convertSlice convert slice with interface{} to slice with strings
   292  func convertSlice(data []interface{}) []string {
   293  	var result []string
   294  
   295  	for _, item := range data {
   296  		result = append(result, fmt.Sprintf("%v", item))
   297  	}
   298  
   299  	return result
   300  }
   301  
   302  // calculateColumnSizes calculate size for each column
   303  func calculateColumnSizes(t *Table) {
   304  	totalColumns := getColumnsNum(t)
   305  	t.columnSizes = make([]int, totalColumns)
   306  
   307  	if len(t.Sizes) != 0 {
   308  		for columnIndex := range t.Sizes {
   309  			if columnIndex < totalColumns {
   310  				t.columnSizes[columnIndex] = t.Sizes[columnIndex]
   311  			}
   312  		}
   313  	}
   314  
   315  	if len(t.data) > 0 {
   316  		for _, row := range t.data {
   317  			for index, item := range row {
   318  				itemSizes := strutil.Len(fmtc.Clean(item))
   319  
   320  				if itemSizes > t.columnSizes[index] {
   321  					t.columnSizes[index] = itemSizes
   322  				}
   323  			}
   324  		}
   325  	}
   326  
   327  	if len(t.Headers) > 0 {
   328  		for index, header := range t.Headers {
   329  			headerSize := strutil.Len(header)
   330  
   331  			if headerSize > t.columnSizes[index] {
   332  				t.columnSizes[index] = headerSize
   333  			}
   334  		}
   335  	}
   336  
   337  	var fullSize int
   338  
   339  	windowWidth := getWindowWidth()
   340  
   341  	for columnIndex, columnSize := range t.columnSizes {
   342  		if columnIndex+1 == totalColumns {
   343  			t.columnSizes[columnIndex] = ((windowWidth - fullSize) - (totalColumns * 3)) + 1
   344  		}
   345  
   346  		fullSize += columnSize
   347  	}
   348  }
   349  
   350  // setColumnsSizes set columns sizes by number of columns
   351  func setColumnsSizes(t *Table, columns int) {
   352  	windowWidth := getWindowWidth()
   353  	t.columnSizes = make([]int, columns)
   354  
   355  	totalSize := 0
   356  	columnSize := (windowWidth / columns) - 3
   357  
   358  	for index := range t.columnSizes {
   359  		t.columnSizes[index] = columnSize
   360  		totalSize += columnSize
   361  
   362  		if index+1 == columns {
   363  			if totalSize+(columns*3) < windowWidth {
   364  				t.columnSizes[index]++
   365  			}
   366  
   367  			t.columnSizes[index]++
   368  		}
   369  	}
   370  }
   371  
   372  // getColumnsNum return number of columns
   373  func getColumnsNum(t *Table) int {
   374  	if len(t.data) > 0 {
   375  		var columns int
   376  
   377  		for _, row := range t.data {
   378  			rowColumns := len(row)
   379  
   380  			if rowColumns > columns {
   381  				columns = rowColumns
   382  			}
   383  		}
   384  
   385  		return columns
   386  	}
   387  
   388  	if len(t.Headers) > 0 {
   389  		return len(t.Headers)
   390  	}
   391  
   392  	return len(t.Sizes)
   393  }
   394  
   395  // formatText align text with color tags
   396  func formatText(data string, size int, align uint8) string {
   397  	var dataSize int
   398  
   399  	if strings.Contains(data, "{") {
   400  		dataSize = strutil.Len(fmtc.Clean(data))
   401  	} else {
   402  		dataSize = strutil.Len(data)
   403  	}
   404  
   405  	if dataSize >= size {
   406  		return data
   407  	}
   408  
   409  	switch align {
   410  	case ALIGN_RIGHT:
   411  		return strings.Repeat(" ", size-dataSize) + data
   412  
   413  	case ALIGN_CENTER:
   414  		prefixSize := (size - dataSize) / 2
   415  		suffixSize := size - (prefixSize + dataSize)
   416  		return strings.Repeat(" ", prefixSize) + data + strings.Repeat(" ", suffixSize)
   417  	}
   418  
   419  	return data + strings.Repeat(" ", size-dataSize)
   420  }
   421  
   422  // getAlignment return align for given column
   423  func getAlignment(t *Table, columnIndex int) uint8 {
   424  	l := len(t.Alignment)
   425  
   426  	if l == 0 || columnIndex >= l {
   427  		return 0
   428  	}
   429  
   430  	return t.Alignment[columnIndex]
   431  }
   432  
   433  // getSeparatorSize return separator size based on size of all columns
   434  func getSeparatorSize(t *Table) int {
   435  	if len(t.columnSizes) == 0 {
   436  		return getWindowWidth()
   437  	}
   438  
   439  	var size int
   440  
   441  	for _, columnSize := range t.columnSizes {
   442  		size += columnSize
   443  	}
   444  
   445  	return size + (len(t.columnSizes) * 3) - 1
   446  }
   447  
   448  // getWindowWidth return window width
   449  func getWindowWidth() int {
   450  	if MaxWidth > 0 {
   451  		return mathutil.Between(MaxWidth, 80, 9999)
   452  	}
   453  
   454  	return mathutil.Between(window.GetWidth(), 80, 9999)
   455  }