blob: a703d7bc280ad420ce51527ac7afdd592f7ab145 [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package texttab
import (
"fmt"
"io"
"sort"
"strings"
"unicode/utf8"
)
// Table does layout of text-based tables.
//
// Many of its methods return the textTable so callers can easily
// chain them to build up many cells at once.
type Table struct {
cells []textCell
cols int
shrink []bool
curRow, curCol int
}
type textCell struct {
row, col, span int
value string
leftMargin string
alignment align
}
type CellOption func(c *textCell)
func LeftMargin(x string) CellOption {
return func(c *textCell) {
c.leftMargin = x
}
}
var (
Left CellOption = func(c *textCell) { c.alignment = alignLeft }
Center = func(c *textCell) { c.alignment = alignCenter }
Right = func(c *textCell) { c.alignment = alignRight }
)
type align int
const (
alignLeft align = iota
alignCenter
alignRight
)
func (a align) lpad(s string, w int) string {
switch a {
default:
return s
case alignCenter:
l := (w - utf8.RuneCountInString(s)) / 2
return fmt.Sprintf("%*s%s", l, "", s)
case alignRight:
return fmt.Sprintf("%*s", w, s)
}
}
// Row starts a new row in table t.
func (t *Table) Row() *Table {
if len(t.cells) > 0 {
t.curRow++
}
t.curCol = 0
return t
}
// Col skips to column "col" in table t. Columns are numbered starting
// at 0.
func (t *Table) Col(col int) *Table {
if col < t.curCol {
panic(fmt.Sprintf("cannot move from column %d to earlier column %d", t.curCol, col))
}
t.curCol = col
return t
}
// CurCol returns the current column index.
func (t *Table) CurCol() int {
return t.curCol
}
// Cell adds a single-column cell at the current row and column.
func (t *Table) Cell(value string, opts ...CellOption) *Table {
return t.Span(1, value, opts...)
}
// Span adds a multi-column cell at the current row and column.
func (t *Table) Span(cols int, value string, opts ...CellOption) *Table {
lMargin := " "
if t.curCol == 0 || len(value) == 0 {
// For the left-most column or empty cells, we default
// to no left margin.
lMargin = ""
}
t.cells = append(t.cells, textCell{t.curRow, t.curCol, cols, value, lMargin, alignLeft})
for _, o := range opts {
o(&t.cells[len(t.cells)-1])
}
t.curCol += cols
if t.curCol > t.cols {
t.cols = t.curCol
}
return t
}
// SetShrink marks a column as a "shrink" column, which will have
// minimum width.
func (t *Table) SetShrink(col int, shrink bool) {
for len(t.shrink) < col+1 {
t.shrink = append(t.shrink, false)
}
t.shrink[col] = shrink
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
// Format lays out table t and writes it to w.
func (t *Table) Format(w io.Writer) error {
shrink := func(col int) bool {
if col < len(t.shrink) {
return t.shrink[col]
}
return false
}
// Collect max length margin for each column.
lmargin := make([]int, t.cols)
for _, cell := range t.cells {
lmargin[cell.col] = max(utf8.RuneCountInString(cell.leftMargin), lmargin[cell.col])
}
// Compute column widths, including their left margins.
ws := make([]int, t.cols)
// Consider cells in increasing span width.
sort.Slice(t.cells, func(i, j int) bool {
return t.cells[i].span < t.cells[j].span
})
var spanCols []int
for _, cell := range t.cells {
w := utf8.RuneCountInString(cell.value) + lmargin[cell.col]
if cell.span == 1 {
// Easy case.
ws[cell.col] = max(ws[cell.col], w)
continue
}
// This cell spans multiple columns. Is the total
// width of those columns already sufficient?
tw := 0
for col := cell.col; col < cell.col+cell.span; col++ {
tw += ws[col]
}
if tw >= w {
continue
}
// We need to expand columns. The goal is to expand
// them all to the necessary average, but some may
// already be wider than this average. Hence, we
// process columns from widest to narrowest,
// subtracting out the columns that are already wider
// than the target average (which in turn changes the
// target), and then distributing the remaining space
// among the narrower ones.
spanCols = spanCols[:0]
for col := cell.col; col < cell.col+cell.span; col++ {
if shrink(col) {
// We can't grow a shrink column, so
// account for its space, but don't
// add it to the columns to adjust.
w -= ws[col]
} else {
spanCols = append(spanCols, col)
}
}
// Process the wider columns first.
sort.Slice(spanCols, func(i, j int) bool {
return ws[spanCols[i]] > ws[spanCols[j]]
})
span := len(spanCols)
for _, col := range spanCols {
// What's the target average width at this
// point? Round up w/span.
avg := (w + span - 1) / span
// Expand column if it isn't wide enough.
ws[col] = max(ws[col], avg)
// Subtract this column from the space needed.
// If the column was already wide enough, this
// will redistribute its excess across the
// smaller columns. We also do this if we
// expanded the column as a convenient way to
// spread out the integer rounding of avg.
w -= ws[col]
span--
}
}
// Convert column widths into starting offsets. The offset of
// column i is where i's left margin begins. The slice
// includes a final offset for the width of the table.
offs := make([]int, t.cols+1)
off := 0
for i, w := range ws {
offs[i] = off
off += w
}
offs[len(ws)] = off
const debugPrintColumns = false
if debugPrintColumns {
fmt.Println(ws)
pos := 0
for col, off := range offs {
fmt.Fprintf(w, "%*s", off-pos, "")
pos = off
for i := 0; i < lmargin[col]; i++ {
fmt.Fprintf(w, "|")
pos++
}
}
fmt.Fprint(w, "\n")
}
// Format the table. Put the cells back into top-to-bottom
// left-to-right order.
sort.Slice(t.cells, func(i, j int) bool {
if t.cells[i].row != t.cells[j].row {
return t.cells[i].row < t.cells[j].row
}
return t.cells[i].col < t.cells[j].col
})
row, off := 0, 0
for _, cell := range t.cells {
if strings.TrimSpace(cell.value) == "" && strings.TrimSpace(cell.leftMargin) == "" {
// Skip empty cells. This avoids printing
// unnecessary trailing spaces if cells appear
// at the end of a row.
continue
}
// Get to cell's row.
for cell.row > row {
if _, err := fmt.Fprintf(w, "\n"); err != nil {
return err
}
row++
off = 0
}
// Space to the cell's starting offset and print its
// left margin.
spaces := offs[cell.col] - off
if _, err := fmt.Fprintf(w, "%*s%*s", spaces, "", lmargin[cell.col], cell.leftMargin); err != nil {
return err
}
off += spaces + lmargin[cell.col]
// Compute total cell width, excluding the margin we
// just printed.
tw := offs[cell.col+cell.span] - offs[cell.col] - lmargin[cell.col]
// Print cell contents.
s := cell.alignment.lpad(cell.value, tw)
if _, err := fmt.Fprintf(w, "%s", s); err != nil {
return err
}
off += utf8.RuneCountInString(s)
}
if len(t.cells) > 0 {
if _, err := fmt.Fprintf(w, "\n"); err != nil {
return err
}
}
return nil
}