mirror of
https://github.com/MickMake/GoSungrow.git
synced 2025-03-31 16:08:04 +02:00
583 lines
16 KiB
Go
583 lines
16 KiB
Go
package gotabulate
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"math"
|
|
"unicode/utf8"
|
|
|
|
"github.com/mattn/go-runewidth"
|
|
)
|
|
|
|
|
|
// TableFormat - Basic Structure of TableFormat
|
|
type TableFormat struct {
|
|
LineTop Line
|
|
LineBelowHeader Line
|
|
LineBetweenRows Line
|
|
LineBottom Line
|
|
HeaderRow Row
|
|
DataRow Row
|
|
TitleRow Row
|
|
Padding int
|
|
HeaderHide bool
|
|
FitScreen bool
|
|
}
|
|
|
|
// Line - Represents a Line
|
|
type Line struct {
|
|
begin string
|
|
hline string
|
|
sep string
|
|
end string
|
|
}
|
|
|
|
// Row - Represents a Row
|
|
type Row struct {
|
|
begin string
|
|
sep string
|
|
end string
|
|
}
|
|
|
|
// TableFormats - Table Formats that are available to the user
|
|
// The user can define his own format, just by adding an entry to this map
|
|
// and calling it with Render function e.g t.Render("customFormat")
|
|
var TableFormats = map[string]TableFormat{
|
|
"simple": TableFormat {
|
|
LineTop: Line{"", "-", " ", ""},
|
|
LineBelowHeader: Line{"", "-", " ", ""},
|
|
LineBottom: Line{"", "-", " ", ""},
|
|
HeaderRow: Row{"", " ", ""},
|
|
DataRow: Row{"", " ", ""},
|
|
TitleRow: Row{"", " ", ""},
|
|
Padding: 1,
|
|
},
|
|
"plain": TableFormat {
|
|
HeaderRow: Row{"", " ", ""},
|
|
DataRow: Row{"", " ", ""},
|
|
TitleRow: Row{"", " ", ""},
|
|
Padding: 1,
|
|
},
|
|
"grid": TableFormat {
|
|
LineTop: Line{"+", "-", "+", "+"},
|
|
LineBelowHeader: Line{"+", "=", "+", "+"},
|
|
LineBetweenRows: Line{"+", "-", "+", "+"},
|
|
LineBottom: Line{"+", "-", "+", "+"},
|
|
HeaderRow: Row{"|", "|", "|"},
|
|
DataRow: Row{"|", "|", "|"},
|
|
TitleRow: Row{"|", " ", "|"},
|
|
Padding: 1,
|
|
},
|
|
"utf8": TableFormat {
|
|
LineTop: Line{"┏", "━", "┳", "┓"},
|
|
LineBelowHeader: Line{"┣", "━", "╇", "┫"},
|
|
// LineBetweenRows: Line{"┣", "━", "╇", "┫"},
|
|
LineBetweenRows: Line{"", "", "", ""},
|
|
LineBottom: Line{"┗", "━", "┷", "┛"},
|
|
HeaderRow: Row{"┃", "┃", "┃"},
|
|
DataRow: Row{"┃", "┃", "┃"},
|
|
TitleRow: Row{"┃", "┃", "┃"},
|
|
Padding: 1,
|
|
},
|
|
"mick": TableFormat {
|
|
LineTop: Line{"┏", "—", "┳", "┓"},
|
|
LineBelowHeader: Line{"|", "—", "╇", "|"},
|
|
// LineBetweenRows: Line{"┃", "━", "╇", "┃"},
|
|
LineBetweenRows: Line{"", "", "", ""},
|
|
LineBottom: Line{"┗", "—", "┷", "┛"},
|
|
HeaderRow: Row{"|", "|", "|"},
|
|
DataRow: Row{"|", "|", "|"},
|
|
TitleRow: Row{"|", "|", "|"},
|
|
Padding: 1,
|
|
},
|
|
"condensed": TableFormat {
|
|
LineTop: Line{"", "-", " ", ""},
|
|
LineBelowHeader: Line{"", "-", " ", ""},
|
|
LineBottom: Line{"", "-", " ", ""},
|
|
HeaderRow: Row{"", " ", ""},
|
|
DataRow: Row{"", " ", ""},
|
|
TitleRow: Row{"", " ", ""},
|
|
Padding: 1,
|
|
},
|
|
"markdown": TableFormat {
|
|
LineTop: Line{"", "-", " ", ""},
|
|
LineBelowHeader: Line{"", "-", " ", ""},
|
|
LineBottom: Line{"", "-", " ", ""},
|
|
HeaderRow: Row{"", " ", ""},
|
|
DataRow: Row{"", " ", ""},
|
|
TitleRow: Row{"", " ", ""},
|
|
Padding: 1,
|
|
},
|
|
}
|
|
|
|
// MinPadding - Minimum padding that will be applied
|
|
var MinPadding = 5
|
|
|
|
// Tabulate - Main Tabulate structure
|
|
type Tabulate struct {
|
|
Data []*TabulateRow
|
|
Title string
|
|
TitleAlign string
|
|
Headers []string
|
|
FloatFormat byte
|
|
TableFormat TableFormat
|
|
Align string
|
|
EmptyVar string
|
|
HideLines []string
|
|
MaxSize int
|
|
WrapStrings bool
|
|
WrapDelimiter rune
|
|
SplitConcat string
|
|
DenseMode bool
|
|
}
|
|
|
|
// TabulateRow - Represents normalized tabulate Row
|
|
type TabulateRow struct {
|
|
Elements []string
|
|
Continuos bool
|
|
}
|
|
|
|
type writeBuffer struct {
|
|
Buffer bytes.Buffer
|
|
}
|
|
|
|
func createBuffer() *writeBuffer {
|
|
return &writeBuffer{}
|
|
}
|
|
|
|
func (b *writeBuffer) Write(str string, count int) *writeBuffer {
|
|
for i := 0; i < count; i++ {
|
|
b.Buffer.WriteString(str)
|
|
}
|
|
return b
|
|
}
|
|
func (b *writeBuffer) String() string {
|
|
return b.Buffer.String()
|
|
}
|
|
|
|
// Add padding to each cell
|
|
func (t *Tabulate) padRow(arr []string, padding int) []string {
|
|
if len(arr) < 1 {
|
|
return arr
|
|
}
|
|
padded := make([]string, len(arr))
|
|
for index, el := range arr {
|
|
b := createBuffer()
|
|
b.Write(" ", padding)
|
|
b.Write(el, 1)
|
|
b.Write(" ", padding)
|
|
padded[index] = b.String()
|
|
}
|
|
return padded
|
|
}
|
|
|
|
// Align right (Add padding left)
|
|
func (t *Tabulate) padLeft(width int, str string) string {
|
|
b := createBuffer()
|
|
b.Write(" ", (width - runewidth.StringWidth(str)))
|
|
b.Write(str, 1)
|
|
return b.String()
|
|
}
|
|
|
|
// Align Left (Add padding right)
|
|
func (t *Tabulate) padRight(width int, str string) string {
|
|
b := createBuffer()
|
|
b.Write(str, 1)
|
|
b.Write(" ", (width - runewidth.StringWidth(str)))
|
|
return b.String()
|
|
}
|
|
|
|
// Center the element in the cell
|
|
func (t *Tabulate) padCenter(width int, str string) string {
|
|
b := createBuffer()
|
|
padding := int(math.Ceil(float64((width - runewidth.StringWidth(str))) / 2.0))
|
|
b.Write(" ", padding)
|
|
b.Write(str, 1)
|
|
b.Write(" ", (width - runewidth.StringWidth(b.String())))
|
|
|
|
return b.String()
|
|
}
|
|
|
|
// Build Line based on padded_widths from t.GetWidths()
|
|
func (t *Tabulate) buildLine(padded_widths []int, padding []int, l Line) string {
|
|
if l.begin == "" && l.hline == "" && l.sep == "" && l.end == "" {
|
|
return ""
|
|
}
|
|
|
|
cells := make([]string, len(padded_widths))
|
|
|
|
for i, _ := range cells {
|
|
b := createBuffer()
|
|
b.Write(l.hline, padding[i]+MinPadding)
|
|
cells[i] = b.String()
|
|
}
|
|
|
|
var buffer bytes.Buffer
|
|
buffer.WriteString(l.begin)
|
|
|
|
// Print contents
|
|
for i := 0; i < len(cells); i++ {
|
|
buffer.WriteString(cells[i])
|
|
if i != len(cells)-1 {
|
|
buffer.WriteString(l.sep)
|
|
}
|
|
}
|
|
|
|
buffer.WriteString(l.end)
|
|
return buffer.String()
|
|
}
|
|
|
|
// buildRow - based on padded_widths from t.GetWidths()
|
|
func (t *Tabulate) buildRow(elements []string, padded_widths []int, paddings []int, d Row) string {
|
|
|
|
var buffer bytes.Buffer
|
|
buffer.WriteString(d.begin)
|
|
padFunc := t.getAlignFunc()
|
|
// Print contents
|
|
for i := 0; i < len(padded_widths); i++ {
|
|
output := ""
|
|
if len(elements) <= i || (len(elements) > i && elements[i] == " nil ") {
|
|
output = padFunc(padded_widths[i], t.EmptyVar)
|
|
} else if len(elements) > i {
|
|
output = padFunc(padded_widths[i], elements[i])
|
|
}
|
|
buffer.WriteString(output)
|
|
if i != len(padded_widths)-1 {
|
|
buffer.WriteString(d.sep)
|
|
}
|
|
}
|
|
|
|
buffer.WriteString(d.end)
|
|
return buffer.String()
|
|
}
|
|
|
|
// SetWrapDelimiter - assigns the character ina string that the rednderer
|
|
// will attempt to split strings on when a cell must be wrapped
|
|
func (t *Tabulate) SetWrapDelimiter(r rune) {
|
|
t.WrapDelimiter = r
|
|
}
|
|
|
|
// SetSplitConcat - assigns the character that will be used when a WrapDelimiter is
|
|
// set but the renderer cannot abide by the desired split. This may happen when
|
|
// the WrapDelimiter is a space ' ' but a single word is longer than the width of a cell
|
|
func (t *Tabulate) SetSplitConcat(r string) {
|
|
t.SplitConcat = r
|
|
}
|
|
|
|
// Render - the data table
|
|
func (t *Tabulate) Render(format ...interface{}) string {
|
|
var lines []string
|
|
|
|
// If headers are set use them, otherwise pop the first row
|
|
if len(t.Headers) < 1 && len(t.Data) > 1 {
|
|
t.Headers, t.Data = t.Data[0].Elements, t.Data[1:]
|
|
}
|
|
|
|
// Use the format that was passed as parameter, otherwise
|
|
// use the format defined in the struct
|
|
if len(format) > 0 {
|
|
t.TableFormat = TableFormats[format[0].(string)]
|
|
}
|
|
|
|
// If Wrap Strings is set to True,then break up the string to multiple cells
|
|
if t.WrapStrings {
|
|
t.Data = t.wrapCellData()
|
|
}
|
|
|
|
// Check if Data is present
|
|
if len(t.Data) < 1 {
|
|
return ""
|
|
}
|
|
|
|
if len(t.Headers) < len(t.Data[0].Elements) {
|
|
diff := len(t.Data[0].Elements) - len(t.Headers)
|
|
padded_header := make([]string, diff)
|
|
for _, e := range t.Headers {
|
|
padded_header = append(padded_header, e)
|
|
}
|
|
t.Headers = padded_header
|
|
}
|
|
|
|
// Get Column widths for all columns
|
|
cols := t.getWidths(t.Headers, t.Data)
|
|
|
|
padded_widths := make([]int, len(cols))
|
|
for i, _ := range padded_widths {
|
|
padded_widths[i] = cols[i] + MinPadding*t.TableFormat.Padding
|
|
}
|
|
|
|
// Calculate total width of the table
|
|
totalWidth := len(t.TableFormat.DataRow.sep) * (len(cols) - 1) // Include all but the final separator
|
|
for _, w := range padded_widths {
|
|
totalWidth += w
|
|
}
|
|
|
|
// Start appending lines
|
|
if len(t.Title) > 0 {
|
|
if !inSlice("aboveTitle", t.HideLines) {
|
|
lines = append(lines, t.buildLine(padded_widths, cols, t.TableFormat.LineTop))
|
|
}
|
|
savedAlign := t.Align
|
|
if len(t.TitleAlign) > 0 {
|
|
t.SetAlign(t.TitleAlign) // Temporary replace alignment with the title alignment
|
|
}
|
|
lines = append(lines, t.buildRow([]string{t.Title}, []int{totalWidth}, nil, t.TableFormat.TitleRow))
|
|
t.SetAlign(savedAlign)
|
|
}
|
|
|
|
// Append top line if not hidden
|
|
if !inSlice("top", t.HideLines) {
|
|
lines = append(lines, t.buildLine(padded_widths, cols, t.TableFormat.LineTop))
|
|
}
|
|
|
|
// Add Header
|
|
lines = append(lines, t.buildRow(t.padRow(t.Headers, t.TableFormat.Padding), padded_widths, cols, t.TableFormat.HeaderRow))
|
|
|
|
// Add Line Below Header if not hidden
|
|
if !inSlice("belowheader", t.HideLines) {
|
|
lines = append(lines, t.buildLine(padded_widths, cols, t.TableFormat.LineBelowHeader))
|
|
}
|
|
|
|
// Add Data Rows
|
|
for index, element := range t.Data {
|
|
lines = append(lines, t.buildRow(t.padRow(element.Elements, t.TableFormat.Padding), padded_widths, cols, t.TableFormat.DataRow))
|
|
if !t.DenseMode && index < len(t.Data)-1 {
|
|
if element.Continuos != true && !inSlice("betweenLine", t.HideLines) {
|
|
// if t.TableFormat.LineBetweenRows.begin == "" &&
|
|
// t.TableFormat.LineBetweenRows.hline == "" &&
|
|
// t.TableFormat.LineBetweenRows.sep == "" &&
|
|
// t.TableFormat.LineBetweenRows.end == "" {
|
|
// } else {
|
|
lines = append(lines, t.buildLine(padded_widths, cols, t.TableFormat.LineBetweenRows))
|
|
// }
|
|
}
|
|
}
|
|
}
|
|
|
|
if !inSlice("bottomLine", t.HideLines) {
|
|
lines = append(lines, t.buildLine(padded_widths, cols, t.TableFormat.LineBottom))
|
|
}
|
|
|
|
// Join lines
|
|
var buffer bytes.Buffer
|
|
for _, line := range lines {
|
|
buffer.WriteString(line + "\n")
|
|
}
|
|
|
|
return buffer.String()
|
|
}
|
|
|
|
// Calculate the max column width for each element
|
|
func (t *Tabulate) getWidths(headers []string, data []*TabulateRow) []int {
|
|
widths := make([]int, len(headers))
|
|
current_max := len(t.EmptyVar)
|
|
for i := 0; i < len(headers); i++ {
|
|
current_max = runewidth.StringWidth(headers[i])
|
|
for _, item := range data {
|
|
if len(item.Elements) > i && len(widths) > i {
|
|
element := item.Elements[i]
|
|
strLength := runewidth.StringWidth(element)
|
|
if strLength > current_max {
|
|
widths[i] = strLength
|
|
current_max = strLength
|
|
} else {
|
|
widths[i] = current_max
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return widths
|
|
}
|
|
|
|
// SetTitle sets the title of the table can also accept a second string to define an alignment for the title
|
|
func (t *Tabulate) SetTitle(title ...string) *Tabulate {
|
|
|
|
t.Title = title[0]
|
|
if len(title) > 1 {
|
|
t.TitleAlign = title[1]
|
|
}
|
|
|
|
return t
|
|
}
|
|
|
|
// SetHeaders - Set Headers of the table
|
|
// If Headers count is less than the data row count, the headers will be padded to the right
|
|
func (t *Tabulate) SetHeaders(headers []string) *Tabulate {
|
|
t.Headers = headers
|
|
return t
|
|
}
|
|
|
|
// SetFloatFormat - Set Float Formatting
|
|
// will be used in strconv.FormatFloat(element, format, -1, 64)
|
|
func (t *Tabulate) SetFloatFormat(format byte) *Tabulate {
|
|
t.FloatFormat = format
|
|
return t
|
|
}
|
|
|
|
// SetAlign - Set Align Type, Available options: left, right, center
|
|
func (t *Tabulate) SetAlign(align string) {
|
|
t.Align = align
|
|
}
|
|
|
|
// Select the padding function based on the align type
|
|
func (t *Tabulate) getAlignFunc() func(int, string) string {
|
|
if len(t.Align) < 1 || t.Align == "right" {
|
|
return t.padLeft
|
|
} else if t.Align == "left" {
|
|
return t.padRight
|
|
} else {
|
|
return t.padCenter
|
|
}
|
|
}
|
|
|
|
// SetEmptyString - Set how an empty cell will be represented
|
|
func (t *Tabulate) SetEmptyString(empty string) {
|
|
t.EmptyVar = empty + " "
|
|
}
|
|
|
|
// SetHideLines - Set which lines to hide.
|
|
// Can be:
|
|
// top - Top line of the table,
|
|
// belowheader - Line below the header,
|
|
// bottomLine - Bottom line of the table
|
|
// betweenLine - Between line of the table
|
|
func (t *Tabulate) SetHideLines(hide []string) {
|
|
t.HideLines = hide
|
|
}
|
|
|
|
func (t *Tabulate) SetWrapStrings(wrap bool) {
|
|
t.WrapStrings = wrap
|
|
}
|
|
|
|
// SetMaxCellSize - Sets the maximum size of cell
|
|
// If WrapStrings is set to true, then the string inside
|
|
// the cell will be split up into multiple cell
|
|
func (t *Tabulate) SetMaxCellSize(max int) {
|
|
t.MaxSize = max
|
|
}
|
|
|
|
// SetDenseMode - Sets dense mode
|
|
// Under dense mode, no space line between rows
|
|
func (t *Tabulate) SetDenseMode() {
|
|
t.DenseMode = true
|
|
}
|
|
|
|
func (t *Tabulate) splitElement(e string) (bool, string) {
|
|
// check if we are not attempting to smartly wrap
|
|
if t.WrapDelimiter == 0 {
|
|
if t.SplitConcat == "" {
|
|
return false, runewidth.Truncate(e, t.MaxSize, "")
|
|
} else {
|
|
return false, runewidth.Truncate(e, t.MaxSize, t.SplitConcat)
|
|
}
|
|
}
|
|
|
|
// we are attempting to wrap
|
|
// grab the current width
|
|
var i int
|
|
for i = t.MaxSize; i > 1; i-- {
|
|
// loop through our proposed truncation size looking for one that ends on
|
|
// our requested delimiter
|
|
x := runewidth.Truncate(e, i, "")
|
|
// check if the NEXT string is a
|
|
// delimiter, if it IS, then we truncate and tell the caller to shrink
|
|
r, _ := utf8.DecodeRuneInString(e[i:])
|
|
if r == 0 || r == 1 {
|
|
// decode failed, take the truncation as is
|
|
return false, x
|
|
}
|
|
if r == t.WrapDelimiter {
|
|
return true, x // inform the caller that they can remove the next rune
|
|
}
|
|
}
|
|
// didn't find a good length, truncate at will
|
|
if t.SplitConcat != "" {
|
|
return false, runewidth.Truncate(e, t.MaxSize, t.SplitConcat)
|
|
}
|
|
return false, runewidth.Truncate(e, t.MaxSize, "")
|
|
}
|
|
|
|
// If string size is larger than t.MaxSize, then split it to multiple cells (downwards)
|
|
func (t *Tabulate) wrapCellData() []*TabulateRow {
|
|
var arr []*TabulateRow
|
|
var cleanSplit bool
|
|
var addr int
|
|
if len(t.Data) == 0 {
|
|
return arr
|
|
}
|
|
next := t.Data[0]
|
|
for index := 0; index <= len(t.Data); index++ {
|
|
elements := next.Elements
|
|
new_elements := make([]string, len(elements))
|
|
|
|
for i, e := range elements {
|
|
if runewidth.StringWidth(e) > t.MaxSize {
|
|
elements[i] = runewidth.Truncate(e, t.MaxSize, "")
|
|
cleanSplit, elements[i] = t.splitElement(e)
|
|
if cleanSplit {
|
|
// remove the next rune
|
|
r, w := utf8.DecodeRuneInString(e[len(elements[i]):])
|
|
if r != 0 && r != 1 {
|
|
addr = w
|
|
}
|
|
} else {
|
|
addr = 0
|
|
}
|
|
new_elements[i] = e[len(elements[i])+addr:]
|
|
next.Continuos = true
|
|
}
|
|
}
|
|
|
|
if next.Continuos {
|
|
arr = append(arr, next)
|
|
next = &TabulateRow{Elements: new_elements}
|
|
index--
|
|
} else if index+1 < len(t.Data) {
|
|
arr = append(arr, next)
|
|
next = t.Data[index+1]
|
|
} else if index >= len(t.Data) {
|
|
arr = append(arr, next)
|
|
}
|
|
|
|
}
|
|
return arr
|
|
}
|
|
|
|
// Create - a new Tabulate Object
|
|
// Accepts 2D String Array, 2D Int Array, 2D Int64 Array,
|
|
// 2D Bool Array, 2D Float64 Array, 2D interface{} Array,
|
|
// Map map[strig]string, Map map[string]interface{},
|
|
func Create(data interface{}) *Tabulate {
|
|
t := &Tabulate{FloatFormat: 'f', MaxSize: 30}
|
|
|
|
switch v := data.(type) {
|
|
case [][]string:
|
|
t.Data = createFromString(data.([][]string))
|
|
case [][]int32:
|
|
t.Data = createFromInt32(data.([][]int32))
|
|
case [][]int64:
|
|
t.Data = createFromInt64(data.([][]int64))
|
|
case [][]int:
|
|
t.Data = createFromInt(data.([][]int))
|
|
case [][]bool:
|
|
t.Data = createFromBool(data.([][]bool))
|
|
case [][]float64:
|
|
t.Data = createFromFloat64(data.([][]float64), t.FloatFormat)
|
|
case [][]interface{}:
|
|
t.Data = createFromMixed(data.([][]interface{}), t.FloatFormat)
|
|
case []string:
|
|
t.Data = createFromString([][]string{data.([]string)})
|
|
case []interface{}:
|
|
t.Data = createFromMixed([][]interface{}{data.([]interface{})}, t.FloatFormat)
|
|
case map[string][]interface{}:
|
|
t.Headers, t.Data = createFromMapMixed(data.(map[string][]interface{}), t.FloatFormat)
|
|
case map[string][]string:
|
|
t.Headers, t.Data = createFromMapString(data.(map[string][]string))
|
|
default:
|
|
fmt.Println(v)
|
|
}
|
|
|
|
return t
|
|
}
|