1// Package table provides a convenient way to generate tabular output of any
2// data, primarily useful for CLI tools.
3//
4// Columns are left-aligned and padded to accomodate the largest cell in that
5// column.
6//
7// Source: https://github.com/rodaine/table
8//
9//   table.DefaultHeaderFormatter = func(format string, vals ...interface{}) string {
10//     return strings.ToUpper(fmt.Sprintf(format, vals...))
11//   }
12//
13//   tbl := table.New("ID", "Name", "Cost ($)")
14//
15//   for _, widget := range Widgets {
16//     tbl.AddRow(widget.ID, widget.Name, widget.Cost)
17//   }
18//
19//   tbl.Print()
20//
21//   // Output:
22//   // ID  NAME      COST ($)
23//   // 1   Foobar    1.23
24//   // 2   Fizzbuzz  4.56
25//   // 3   Gizmo     78.90
26package table
27
28import (
29	"fmt"
30	"io"
31	"os"
32	"strings"
33	"unicode/utf8"
34)
35
36// These are the default properties for all Tables created from this package
37// and can be modified.
38var (
39	// DefaultPadding specifies the number of spaces between columns in a table.
40	DefaultPadding = 2
41
42	// DefaultWriter specifies the output io.Writer for the Table.Print method.
43	DefaultWriter io.Writer = os.Stdout
44
45	// DefaultHeaderFormatter specifies the default Formatter for the table header.
46	DefaultHeaderFormatter Formatter
47
48	// DefaultFirstColumnFormatter specifies the default Formatter for the first column cells.
49	DefaultFirstColumnFormatter Formatter
50
51	// DefaultWidthFunc specifies the default WidthFunc for calculating column widths
52	DefaultWidthFunc WidthFunc = utf8.RuneCountInString
53)
54
55// Formatter functions expose a fmt.Sprintf signature that can be used to modify
56// the display of the text in either the header or first column of a Table.
57// The formatter should not change the width of original text as printed since
58// column widths are calculated pre-formatting (though this issue can be mitigated
59// with increased padding).
60//
61//   tbl.WithHeaderFormatter(func(format string, vals ...interface{}) string {
62//     return strings.ToUpper(fmt.Sprintf(format, vals...))
63//   })
64//
65// A good use case for formatters is to use ANSI escape codes to color the cells
66// for a nicer interface. The package color (https://github.com/fatih/color) makes
67// it easy to generate these automatically: http://godoc.org/github.com/fatih/color#Color.SprintfFunc
68type Formatter func(string, ...interface{}) string
69
70// A WidthFunc calculates the width of a string. By default, the number of runes
71// is used but this may not be appropriate for certain character sets. The
72// package runewidth (https://github.com/mattn/go-runewidth) could be used to
73// accomodate multi-cell characters (such as emoji or CJK characters).
74type WidthFunc func(string) int
75
76// Table describes the interface for building up a tabular representation of data.
77// It exposes fluent/chainable methods for convenient table building.
78//
79// WithHeaderFormatter and WithFirstColumnFormatter sets the Formatter for the
80// header and first column, respectively. If nil is passed in (the default), no
81// formatting will be applied.
82//
83//   New("foo", "bar").WithFirstColumnFormatter(func(f string, v ...interface{}) string {
84//     return strings.ToUpper(fmt.Sprintf(f, v...))
85//   })
86//
87// WithPadding specifies the minimum padding between cells in a row and defaults
88// to DefaultPadding. Padding values less than or equal to zero apply no extra
89// padding between the columns.
90//
91//   New("foo", "bar").WithPadding(3)
92//
93// WithWriter modifies the writer which Print outputs to, defaulting to DefaultWriter
94// when instantiated. If nil is passed, os.Stdout will be used.
95//
96//   New("foo", "bar").WithWriter(os.Stderr)
97//
98// WithWidthFunc sets the function used to calculate the width of the string in
99// a column. By default, the number of utf8 runes in the string is used.
100//
101// AddRow adds another row of data to the table. Any values can be passed in and
102// will be output as its string representation as described in the fmt standard
103// package. Rows can have less cells than the total number of columns in the table;
104// subsequent cells will be rendered empty. Rows with more cells than the total
105// number of columns will be truncated. References to the data are not held, so
106// the passed in values can be modified without affecting the table's output.
107//
108//   New("foo", "bar").AddRow("fizz", "buzz").AddRow(time.Now()).AddRow(1, 2, 3).Print()
109//   // Output:
110//   // foo                              bar
111//   // fizz                             buzz
112//   // 2006-01-02 15:04:05.0 -0700 MST
113//   // 1                                2
114//
115// Print writes the string representation of the table to the provided writer.
116// Print can be called multiple times, even after subsequent mutations of the
117// provided data. The output is always preceded and followed by a new line.
118type Table interface {
119	WithHeaderFormatter(f Formatter) Table
120	WithFirstColumnFormatter(f Formatter) Table
121	WithPadding(p int) Table
122	WithWriter(w io.Writer) Table
123	WithWidthFunc(f WidthFunc) Table
124
125	AddRow(vals ...interface{}) Table
126	Print(includeHeader bool)
127}
128
129// New creates a Table instance with the specified header(s) provided. The number
130// of columns is fixed at this point to len(columnHeaders) and the defined defaults
131// are set on the instance.
132func New(columnHeaders ...interface{}) Table {
133	t := table{header: make([]string, len(columnHeaders))}
134
135	t.WithPadding(DefaultPadding)
136	t.WithWriter(DefaultWriter)
137	t.WithHeaderFormatter(DefaultHeaderFormatter)
138	t.WithFirstColumnFormatter(DefaultFirstColumnFormatter)
139	t.WithWidthFunc(DefaultWidthFunc)
140
141	for i, col := range columnHeaders {
142		t.header[i] = fmt.Sprint(col)
143	}
144
145	return &t
146}
147
148type table struct {
149	FirstColumnFormatter Formatter
150	HeaderFormatter      Formatter
151	Padding              int
152	Writer               io.Writer
153	Width                WidthFunc
154
155	header []string
156	rows   [][]string
157	widths []int
158}
159
160func (t *table) WithHeaderFormatter(f Formatter) Table {
161	t.HeaderFormatter = f
162	return t
163}
164
165func (t *table) WithFirstColumnFormatter(f Formatter) Table {
166	t.FirstColumnFormatter = f
167	return t
168}
169
170func (t *table) WithPadding(p int) Table {
171	if p < 0 {
172		p = 0
173	}
174
175	t.Padding = p
176	return t
177}
178
179func (t *table) WithWriter(w io.Writer) Table {
180	if w == nil {
181		w = os.Stdout
182	}
183
184	t.Writer = w
185	return t
186}
187
188func (t *table) WithWidthFunc(f WidthFunc) Table {
189	t.Width = f
190	return t
191}
192
193func (t *table) AddRow(vals ...interface{}) Table {
194	row := make([]string, len(t.header))
195	for i, val := range vals {
196		if i >= len(t.header) {
197			break
198		}
199		row[i] = fmt.Sprint(val)
200	}
201	t.rows = append(t.rows, row)
202
203	return t
204}
205
206func (t *table) Print(includeHeader bool) {
207	format := strings.Repeat("%s", len(t.header)) + "\n"
208	t.calculateWidths()
209
210	if includeHeader {
211		t.printHeader(format)
212	}
213	for _, row := range t.rows {
214		t.printRow(format, row)
215	}
216}
217
218func (t *table) printHeader(format string) {
219	vals := t.applyWidths(t.header, t.widths)
220	if t.HeaderFormatter != nil {
221		txt := t.HeaderFormatter(format, vals...)
222		fmt.Fprint(t.Writer, txt)
223	} else {
224		fmt.Fprintf(t.Writer, format, vals...)
225	}
226}
227
228func (t *table) printRow(format string, row []string) {
229	vals := t.applyWidths(row, t.widths)
230
231	if t.FirstColumnFormatter != nil {
232		vals[0] = t.FirstColumnFormatter("%s", vals[0])
233	}
234
235	fmt.Fprintf(t.Writer, format, vals...)
236}
237
238func (t *table) calculateWidths() {
239	t.widths = make([]int, len(t.header))
240	for _, row := range t.rows {
241		for i, v := range row {
242			if w := t.Width(v) + t.Padding; w > t.widths[i] {
243				t.widths[i] = w
244			}
245		}
246	}
247
248	for i, v := range t.header {
249		if w := t.Width(v) + t.Padding; w > t.widths[i] {
250			t.widths[i] = w
251		}
252	}
253}
254
255func (t *table) applyWidths(row []string, widths []int) []interface{} {
256	out := make([]interface{}, len(row))
257	for i, s := range row {
258		out[i] = s + t.lenOffset(s, widths[i])
259	}
260	return out
261}
262
263func (t *table) lenOffset(s string, w int) string {
264	l := w - t.Width(s)
265	if l <= 0 {
266		return ""
267	}
268	return strings.Repeat(" ", l)
269}
270