|
|
@@ -9,6 +9,7 @@
|
|
|
package tablewriter
|
|
|
|
|
|
import (
|
|
|
+ "bytes"
|
|
|
"fmt"
|
|
|
"io"
|
|
|
"regexp"
|
|
|
@@ -20,45 +21,57 @@ const (
|
|
|
)
|
|
|
|
|
|
const (
|
|
|
- CENTRE = "+"
|
|
|
- ROW = "-"
|
|
|
- COLUMN = "|"
|
|
|
- SPACE = " "
|
|
|
+ CENTER = "+"
|
|
|
+ ROW = "-"
|
|
|
+ COLUMN = "|"
|
|
|
+ SPACE = " "
|
|
|
+ NEWLINE = "\n"
|
|
|
)
|
|
|
|
|
|
const (
|
|
|
ALIGN_DEFAULT = iota
|
|
|
- ALIGN_CENTRE
|
|
|
+ ALIGN_CENTER
|
|
|
ALIGN_RIGHT
|
|
|
ALIGN_LEFT
|
|
|
)
|
|
|
|
|
|
var (
|
|
|
- decimal = regexp.MustCompile(`^\d*\.?\d*$`)
|
|
|
- percent = regexp.MustCompile(`^\d*\.?\d*$%$`)
|
|
|
+ decimal = regexp.MustCompile(`^-*\d*\.?\d*$`)
|
|
|
+ percent = regexp.MustCompile(`^-*\d*\.?\d*$%$`)
|
|
|
)
|
|
|
|
|
|
+type Border struct {
|
|
|
+ Left bool
|
|
|
+ Right bool
|
|
|
+ Top bool
|
|
|
+ Bottom bool
|
|
|
+}
|
|
|
+
|
|
|
type Table struct {
|
|
|
- out io.Writer
|
|
|
- rows [][]string
|
|
|
- lines [][][]string
|
|
|
- cs map[int]int
|
|
|
- rs map[int]int
|
|
|
- headers []string
|
|
|
- footers []string
|
|
|
- autoFmt bool
|
|
|
- autoWrap bool
|
|
|
- mW int
|
|
|
- pCenter string
|
|
|
- pRow string
|
|
|
- pColumn string
|
|
|
- tColumn int
|
|
|
- tRow int
|
|
|
- align int
|
|
|
- rowLine bool
|
|
|
- hdrLine bool
|
|
|
- border bool
|
|
|
- colSize int
|
|
|
+ out io.Writer
|
|
|
+ rows [][]string
|
|
|
+ lines [][][]string
|
|
|
+ cs map[int]int
|
|
|
+ rs map[int]int
|
|
|
+ headers []string
|
|
|
+ footers []string
|
|
|
+ autoFmt bool
|
|
|
+ autoWrap bool
|
|
|
+ mW int
|
|
|
+ pCenter string
|
|
|
+ pRow string
|
|
|
+ pColumn string
|
|
|
+ tColumn int
|
|
|
+ tRow int
|
|
|
+ hAlign int
|
|
|
+ fAlign int
|
|
|
+ align int
|
|
|
+ newLine string
|
|
|
+ rowLine bool
|
|
|
+ autoMergeCells bool
|
|
|
+ hdrLine bool
|
|
|
+ borders Border
|
|
|
+ colSize int
|
|
|
}
|
|
|
|
|
|
// Start New Table
|
|
|
@@ -75,28 +88,35 @@ func NewWriter(writer io.Writer) *Table {
|
|
|
autoFmt: true,
|
|
|
autoWrap: true,
|
|
|
mW: MAX_ROW_WIDTH,
|
|
|
- pCenter: CENTRE,
|
|
|
+ pCenter: CENTER,
|
|
|
pRow: ROW,
|
|
|
pColumn: COLUMN,
|
|
|
tColumn: -1,
|
|
|
tRow: -1,
|
|
|
+ hAlign: ALIGN_DEFAULT,
|
|
|
+ fAlign: ALIGN_DEFAULT,
|
|
|
align: ALIGN_DEFAULT,
|
|
|
+ newLine: NEWLINE,
|
|
|
rowLine: false,
|
|
|
hdrLine: true,
|
|
|
- border: true,
|
|
|
+ borders: Border{Left: true, Right: true, Bottom: true, Top: true},
|
|
|
colSize: -1}
|
|
|
return t
|
|
|
}
|
|
|
|
|
|
// Render table output
|
|
|
func (t Table) Render() {
|
|
|
- if t.border {
|
|
|
+ if t.borders.Top {
|
|
|
t.printLine(true)
|
|
|
}
|
|
|
t.printHeading()
|
|
|
- t.printRows()
|
|
|
+ if t.autoMergeCells {
|
|
|
+ t.printRowsMergeCells()
|
|
|
+ } else {
|
|
|
+ t.printRows()
|
|
|
+ }
|
|
|
|
|
|
- if !t.rowLine && t.border {
|
|
|
+ if !t.rowLine && t.borders.Bottom {
|
|
|
t.printLine(true)
|
|
|
}
|
|
|
t.printFooter()
|
|
|
@@ -151,11 +171,26 @@ func (t *Table) SetCenterSeparator(sep string) {
|
|
|
t.pCenter = sep
|
|
|
}
|
|
|
|
|
|
+// Set Header Alignment
|
|
|
+func (t *Table) SetHeaderAlignment(hAlign int) {
|
|
|
+ t.hAlign = hAlign
|
|
|
+}
|
|
|
+
|
|
|
+// Set Footer Alignment
|
|
|
+func (t *Table) SetFooterAlignment(fAlign int) {
|
|
|
+ t.fAlign = fAlign
|
|
|
+}
|
|
|
+
|
|
|
// Set Table Alignment
|
|
|
func (t *Table) SetAlignment(align int) {
|
|
|
t.align = align
|
|
|
}
|
|
|
|
|
|
+// Set New Line
|
|
|
+func (t *Table) SetNewLine(nl string) {
|
|
|
+ t.newLine = nl
|
|
|
+}
|
|
|
+
|
|
|
// Set Header Line
|
|
|
// This would enable / disable a line after the header
|
|
|
func (t *Table) SetHeaderLine(line bool) {
|
|
|
@@ -168,10 +203,20 @@ func (t *Table) SetRowLine(line bool) {
|
|
|
t.rowLine = line
|
|
|
}
|
|
|
|
|
|
+// Set Auto Merge Cells
|
|
|
+// This would enable / disable the merge of cells with identical values
|
|
|
+func (t *Table) SetAutoMergeCells(auto bool) {
|
|
|
+ t.autoMergeCells = auto
|
|
|
+}
|
|
|
+
|
|
|
// Set Table Border
|
|
|
// This would enable / disable line around the table
|
|
|
func (t *Table) SetBorder(border bool) {
|
|
|
- t.border = border
|
|
|
+ t.SetBorders(Border{border, border, border, border})
|
|
|
+}
|
|
|
+
|
|
|
+func (t *Table) SetBorders(border Border) {
|
|
|
+ t.borders = border
|
|
|
}
|
|
|
|
|
|
// Append row to table
|
|
|
@@ -216,8 +261,45 @@ func (t Table) printLine(nl bool) {
|
|
|
t.pCenter)
|
|
|
}
|
|
|
if nl {
|
|
|
- fmt.Fprintln(t.out)
|
|
|
+ fmt.Fprint(t.out, t.newLine)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Print line based on row width with our without cell separator
|
|
|
+func (t Table) printLineOptionalCellSeparators(nl bool, displayCellSeparator []bool) {
|
|
|
+ fmt.Fprint(t.out, t.pCenter)
|
|
|
+ for i := 0; i < len(t.cs); i++ {
|
|
|
+ v := t.cs[i]
|
|
|
+ if i > len(displayCellSeparator) || displayCellSeparator[i] {
|
|
|
+ // Display the cell separator
|
|
|
+ fmt.Fprintf(t.out, "%s%s%s%s",
|
|
|
+ t.pRow,
|
|
|
+ strings.Repeat(string(t.pRow), v),
|
|
|
+ t.pRow,
|
|
|
+ t.pCenter)
|
|
|
+ } else {
|
|
|
+ // Don't display the cell separator for this cell
|
|
|
+ fmt.Fprintf(t.out, "%s%s",
|
|
|
+ strings.Repeat(" ", v+2),
|
|
|
+ t.pCenter)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if nl {
|
|
|
+ fmt.Fprint(t.out, t.newLine)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Return the PadRight function if align is left, PadLeft if align is right,
|
|
|
+// and Pad by default
|
|
|
+func pad(align int) func(string, string, int) string {
|
|
|
+ padFunc := Pad
|
|
|
+ switch align {
|
|
|
+ case ALIGN_LEFT:
|
|
|
+ padFunc = PadRight
|
|
|
+ case ALIGN_RIGHT:
|
|
|
+ padFunc = PadLeft
|
|
|
}
|
|
|
+ return padFunc
|
|
|
}
|
|
|
|
|
|
// Print heading information
|
|
|
@@ -229,11 +311,14 @@ func (t Table) printHeading() {
|
|
|
|
|
|
// Check if border is set
|
|
|
// Replace with space if not set
|
|
|
- fmt.Fprint(t.out, ConditionString(t.border, t.pColumn, SPACE))
|
|
|
+ fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE))
|
|
|
|
|
|
// Identify last column
|
|
|
end := len(t.cs) - 1
|
|
|
|
|
|
+ // Get pad function
|
|
|
+ padFunc := pad(t.hAlign)
|
|
|
+
|
|
|
// Print Heading column
|
|
|
for i := 0; i <= end; i++ {
|
|
|
v := t.cs[i]
|
|
|
@@ -241,13 +326,13 @@ func (t Table) printHeading() {
|
|
|
if t.autoFmt {
|
|
|
h = Title(h)
|
|
|
}
|
|
|
- pad := ConditionString((i == end && !t.border), SPACE, t.pColumn)
|
|
|
+ pad := ConditionString((i == end && !t.borders.Left), SPACE, t.pColumn)
|
|
|
fmt.Fprintf(t.out, " %s %s",
|
|
|
- Pad(h, SPACE, v),
|
|
|
+ padFunc(h, SPACE, v),
|
|
|
pad)
|
|
|
}
|
|
|
// Next line
|
|
|
- fmt.Fprintln(t.out)
|
|
|
+ fmt.Fprint(t.out, t.newLine)
|
|
|
if t.hdrLine {
|
|
|
t.printLine(true)
|
|
|
}
|
|
|
@@ -261,16 +346,19 @@ func (t Table) printFooter() {
|
|
|
}
|
|
|
|
|
|
// Only print line if border is not set
|
|
|
- if !t.border {
|
|
|
+ if !t.borders.Bottom {
|
|
|
t.printLine(true)
|
|
|
}
|
|
|
// Check if border is set
|
|
|
// Replace with space if not set
|
|
|
- fmt.Fprint(t.out, ConditionString(t.border, t.pColumn, SPACE))
|
|
|
+ fmt.Fprint(t.out, ConditionString(t.borders.Bottom, t.pColumn, SPACE))
|
|
|
|
|
|
// Identify last column
|
|
|
end := len(t.cs) - 1
|
|
|
|
|
|
+ // Get pad function
|
|
|
+ padFunc := pad(t.fAlign)
|
|
|
+
|
|
|
// Print Heading column
|
|
|
for i := 0; i <= end; i++ {
|
|
|
v := t.cs[i]
|
|
|
@@ -278,17 +366,17 @@ func (t Table) printFooter() {
|
|
|
if t.autoFmt {
|
|
|
f = Title(f)
|
|
|
}
|
|
|
- pad := ConditionString((i == end && !t.border), SPACE, t.pColumn)
|
|
|
+ pad := ConditionString((i == end && !t.borders.Top), SPACE, t.pColumn)
|
|
|
|
|
|
if len(t.footers[i]) == 0 {
|
|
|
pad = SPACE
|
|
|
}
|
|
|
fmt.Fprintf(t.out, " %s %s",
|
|
|
- Pad(f, SPACE, v),
|
|
|
+ padFunc(f, SPACE, v),
|
|
|
pad)
|
|
|
}
|
|
|
// Next line
|
|
|
- fmt.Fprintln(t.out)
|
|
|
+ fmt.Fprint(t.out, t.newLine)
|
|
|
//t.printLine(true)
|
|
|
|
|
|
hasPrinted := false
|
|
|
@@ -304,7 +392,7 @@ func (t Table) printFooter() {
|
|
|
}
|
|
|
|
|
|
// Set center to be space if length is 0
|
|
|
- if length == 0 && !t.border {
|
|
|
+ if length == 0 && !t.borders.Right {
|
|
|
center = SPACE
|
|
|
}
|
|
|
|
|
|
@@ -318,7 +406,7 @@ func (t Table) printFooter() {
|
|
|
pad = SPACE
|
|
|
}
|
|
|
// Ignore left space of it has printed before
|
|
|
- if hasPrinted || t.border {
|
|
|
+ if hasPrinted || t.borders.Left {
|
|
|
pad = t.pRow
|
|
|
center = t.pCenter
|
|
|
}
|
|
|
@@ -339,7 +427,7 @@ func (t Table) printFooter() {
|
|
|
|
|
|
}
|
|
|
|
|
|
- fmt.Fprintln(t.out)
|
|
|
+ fmt.Fprint(t.out, t.newLine)
|
|
|
|
|
|
}
|
|
|
|
|
|
@@ -383,7 +471,7 @@ func (t Table) printRow(columns [][]string, colKey int) {
|
|
|
for y := 0; y < total; y++ {
|
|
|
|
|
|
// Check if border is set
|
|
|
- fmt.Fprint(t.out, ConditionString((!t.border && y == 0), SPACE, t.pColumn))
|
|
|
+ fmt.Fprint(t.out, ConditionString((!t.borders.Left && y == 0), SPACE, t.pColumn))
|
|
|
|
|
|
fmt.Fprintf(t.out, SPACE)
|
|
|
str := columns[y][x]
|
|
|
@@ -391,7 +479,7 @@ func (t Table) printRow(columns [][]string, colKey int) {
|
|
|
// This would print alignment
|
|
|
// Default alignment would use multiple configuration
|
|
|
switch t.align {
|
|
|
- case ALIGN_CENTRE: //
|
|
|
+ case ALIGN_CENTER: //
|
|
|
fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y]))
|
|
|
case ALIGN_RIGHT:
|
|
|
fmt.Fprintf(t.out, "%s", PadLeft(str, SPACE, t.cs[y]))
|
|
|
@@ -416,14 +504,111 @@ func (t Table) printRow(columns [][]string, colKey int) {
|
|
|
}
|
|
|
// Check if border is set
|
|
|
// Replace with space if not set
|
|
|
- fmt.Fprint(t.out, ConditionString(t.border, t.pColumn, SPACE))
|
|
|
- fmt.Fprintln(t.out)
|
|
|
+ fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE))
|
|
|
+ fmt.Fprint(t.out, t.newLine)
|
|
|
}
|
|
|
|
|
|
if t.rowLine {
|
|
|
t.printLine(true)
|
|
|
}
|
|
|
+}
|
|
|
+
|
|
|
+// Print the rows of the table and merge the cells that are identical
|
|
|
+func (t Table) printRowsMergeCells() {
|
|
|
+ var previousLine []string
|
|
|
+ var displayCellBorder []bool
|
|
|
+ var tmpWriter bytes.Buffer
|
|
|
+ for i, lines := range t.lines {
|
|
|
+ // We store the display of the current line in a tmp writer, as we need to know which border needs to be print above
|
|
|
+ previousLine, displayCellBorder = t.printRowMergeCells(&tmpWriter, lines, i, previousLine)
|
|
|
+ if i > 0 { //We don't need to print borders above first line
|
|
|
+ if t.rowLine {
|
|
|
+ t.printLineOptionalCellSeparators(true, displayCellBorder)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ tmpWriter.WriteTo(t.out)
|
|
|
+ }
|
|
|
+ //Print the end of the table
|
|
|
+ if t.rowLine {
|
|
|
+ t.printLine(true)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Print Row Information to a writer and merge identical cells.
|
|
|
+// Adjust column alignment based on type
|
|
|
+
|
|
|
+func (t Table) printRowMergeCells(writer io.Writer, columns [][]string, colKey int, previousLine []string) ([]string, []bool) {
|
|
|
+ // Get Maximum Height
|
|
|
+ max := t.rs[colKey]
|
|
|
+ total := len(columns)
|
|
|
+
|
|
|
+ // Pad Each Height
|
|
|
+ pads := []int{}
|
|
|
+
|
|
|
+ for i, line := range columns {
|
|
|
+ length := len(line)
|
|
|
+ pad := max - length
|
|
|
+ pads = append(pads, pad)
|
|
|
+ for n := 0; n < pad; n++ {
|
|
|
+ columns[i] = append(columns[i], " ")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var displayCellBorder []bool
|
|
|
+ for x := 0; x < max; x++ {
|
|
|
+ for y := 0; y < total; y++ {
|
|
|
+
|
|
|
+ // Check if border is set
|
|
|
+ fmt.Fprint(writer, ConditionString((!t.borders.Left && y == 0), SPACE, t.pColumn))
|
|
|
+
|
|
|
+ fmt.Fprintf(writer, SPACE)
|
|
|
+
|
|
|
+ str := columns[y][x]
|
|
|
+
|
|
|
+ if t.autoMergeCells {
|
|
|
+ //Store the full line to merge mutli-lines cells
|
|
|
+ fullLine := strings.Join(columns[y], " ")
|
|
|
+ if len(previousLine) > y && fullLine == previousLine[y] && fullLine != "" {
|
|
|
+ // If this cell is identical to the one above but not empty, we don't display the border and keep the cell empty.
|
|
|
+ displayCellBorder = append(displayCellBorder, false)
|
|
|
+ str = ""
|
|
|
+ } else {
|
|
|
+ // First line or different content, keep the content and print the cell border
|
|
|
+ displayCellBorder = append(displayCellBorder, true)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // This would print alignment
|
|
|
+ // Default alignment would use multiple configuration
|
|
|
+ switch t.align {
|
|
|
+ case ALIGN_CENTER: //
|
|
|
+ fmt.Fprintf(writer, "%s", Pad(str, SPACE, t.cs[y]))
|
|
|
+ case ALIGN_RIGHT:
|
|
|
+ fmt.Fprintf(writer, "%s", PadLeft(str, SPACE, t.cs[y]))
|
|
|
+ case ALIGN_LEFT:
|
|
|
+ fmt.Fprintf(writer, "%s", PadRight(str, SPACE, t.cs[y]))
|
|
|
+ default:
|
|
|
+ if decimal.MatchString(strings.TrimSpace(str)) || percent.MatchString(strings.TrimSpace(str)) {
|
|
|
+ fmt.Fprintf(writer, "%s", PadLeft(str, SPACE, t.cs[y]))
|
|
|
+ } else {
|
|
|
+ fmt.Fprintf(writer, "%s", PadRight(str, SPACE, t.cs[y]))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ fmt.Fprintf(writer, SPACE)
|
|
|
+ }
|
|
|
+ // Check if border is set
|
|
|
+ // Replace with space if not set
|
|
|
+ fmt.Fprint(writer, ConditionString(t.borders.Left, t.pColumn, SPACE))
|
|
|
+ fmt.Fprint(writer, t.newLine)
|
|
|
+ }
|
|
|
|
|
|
+ //The new previous line is the current one
|
|
|
+ previousLine = make([]string, total)
|
|
|
+ for y := 0; y < total; y++ {
|
|
|
+ previousLine[y] = strings.Join(columns[y], " ") //Store the full line for multi-lines cells
|
|
|
+ }
|
|
|
+ //Returns the newly added line and wether or not a border should be displayed above.
|
|
|
+ return previousLine, displayCellBorder
|
|
|
}
|
|
|
|
|
|
func (t *Table) parseDimension(str string, colKey, rowKey int) []string {
|