2022-12-09 20:36:31 +11:00

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
}