From a8aa9fe9fe7344432b062324318efc341d3fa8d0 Mon Sep 17 00:00:00 2001 From: Marcel Schramm Date: Wed, 3 Apr 2024 01:28:50 +0200 Subject: [PATCH] Add `WithHeaderSeparatorRow` (#40) * Add `WithHeaderSeparatorRow` This function will print a separator line between the header and the data rows. It will use the same formatter as the header. The width of each separator cell will be equal to the width of the header cell in the same column. It supports runes, meaning dependning on the rune width, it might not be perfect. * Improve variables names / explain intentions --- table.go | 94 ++++++++++++++++++++++++++++++++++++--------------- table_test.go | 41 ++++++++++++++++++++++ 2 files changed, 108 insertions(+), 27 deletions(-) diff --git a/table.go b/table.go index 0ad6cd6..a0270fe 100644 --- a/table.go +++ b/table.go @@ -6,23 +6,23 @@ // // Source: https://github.com/rodaine/table // -// table.DefaultHeaderFormatter = func(format string, vals ...interface{}) string { -// return strings.ToUpper(fmt.Sprintf(format, vals...)) -// } +// table.DefaultHeaderFormatter = func(format string, vals ...interface{}) string { +// return strings.ToUpper(fmt.Sprintf(format, vals...)) +// } // -// tbl := table.New("ID", "Name", "Cost ($)") +// tbl := table.New("ID", "Name", "Cost ($)") // -// for _, widget := range Widgets { -// tbl.AddRow(widget.ID, widget.Name, widget.Cost) -// } +// for _, widget := range Widgets { +// tbl.AddRow(widget.ID, widget.Name, widget.Cost) +// } // -// tbl.Print() +// tbl.Print() // -// // Output: -// // ID NAME COST ($) -// // 1 Foobar 1.23 -// // 2 Fizzbuzz 4.56 -// // 3 Gizmo 78.90 +// // Output: +// // ID NAME COST ($) +// // 1 Foobar 1.23 +// // 2 Fizzbuzz 4.56 +// // 3 Gizmo 78.90 package table import ( @@ -58,9 +58,9 @@ var ( // column widths are calculated pre-formatting (though this issue can be mitigated // with increased padding). // -// tbl.WithHeaderFormatter(func(format string, vals ...interface{}) string { -// return strings.ToUpper(fmt.Sprintf(format, vals...)) -// }) +// tbl.WithHeaderFormatter(func(format string, vals ...interface{}) string { +// return strings.ToUpper(fmt.Sprintf(format, vals...)) +// }) // // A good use case for formatters is to use ANSI escape codes to color the cells // for a nicer interface. The package color (https://github.com/fatih/color) makes @@ -80,20 +80,20 @@ type WidthFunc func(string) int // header and first column, respectively. If nil is passed in (the default), no // formatting will be applied. // -// New("foo", "bar").WithFirstColumnFormatter(func(f string, v ...interface{}) string { -// return strings.ToUpper(fmt.Sprintf(f, v...)) -// }) +// New("foo", "bar").WithFirstColumnFormatter(func(f string, v ...interface{}) string { +// return strings.ToUpper(fmt.Sprintf(f, v...)) +// }) // // WithPadding specifies the minimum padding between cells in a row and defaults // to DefaultPadding. Padding values less than or equal to zero apply no extra // padding between the columns. // -// New("foo", "bar").WithPadding(3) +// New("foo", "bar").WithPadding(3) // // WithWriter modifies the writer which Print outputs to, defaulting to DefaultWriter // when instantiated. If nil is passed, os.Stdout will be used. // -// New("foo", "bar").WithWriter(os.Stderr) +// New("foo", "bar").WithWriter(os.Stderr) // // WithWidthFunc sets the function used to calculate the width of the string in // a column. By default, the number of utf8 runes in the string is used. @@ -105,12 +105,12 @@ type WidthFunc func(string) int // number of columns will be truncated. References to the data are not held, so // the passed in values can be modified without affecting the table's output. // -// New("foo", "bar").AddRow("fizz", "buzz").AddRow(time.Now()).AddRow(1, 2, 3).Print() -// // Output: -// // foo bar -// // fizz buzz -// // 2006-01-02 15:04:05.0 -0700 MST -// // 1 2 +// New("foo", "bar").AddRow("fizz", "buzz").AddRow(time.Now()).AddRow(1, 2, 3).Print() +// // Output: +// // foo bar +// // fizz buzz +// // 2006-01-02 15:04:05.0 -0700 MST +// // 1 2 // // Print writes the string representation of the table to the provided writer. // Print can be called multiple times, even after subsequent mutations of the @@ -121,6 +121,7 @@ type Table interface { WithPadding(p int) Table WithWriter(w io.Writer) Table WithWidthFunc(f WidthFunc) Table + WithHeaderSeparatorRow(r rune) Table AddRow(vals ...interface{}) Table SetRows(rows [][]string) Table @@ -152,6 +153,7 @@ type table struct { Padding int Writer io.Writer Width WidthFunc + HeaderSeparatorRune rune header []string rows [][]string @@ -163,6 +165,11 @@ func (t *table) WithHeaderFormatter(f Formatter) Table { return t } +func (t *table) WithHeaderSeparatorRow(r rune) Table { + t.HeaderSeparatorRune = r + return t +} + func (t *table) WithFirstColumnFormatter(f Formatter) Table { t.FirstColumnFormatter = f return t @@ -231,11 +238,44 @@ func (t *table) Print() { t.calculateWidths() t.printHeader(format) + if t.HeaderSeparatorRune != 0 { + t.printHeaderSeparator(format) + } for _, row := range t.rows { t.printRow(format, row) } } +func (t *table) printHeaderSeparator(format string) { + separators := make([]string, len(t.header)) + + // The separator could be any unicode char. Since some chars take up more + // than one cell in a monospace context, we can get a number higher than 1 + // here. Am example would be this emoji 🤣. + separatorCellWidth := t.Width(string([]rune{t.HeaderSeparatorRune})) + for index, headerName := range t.header { + headerCellWidth := t.Width(headerName) + // Note that this might not be evenly divisble. In this case we'll get a + // separator that is at least 1 cell shorter than the header. This was + // an intentional design decision in order to prevent widening the cell + // or overstepping the column bounds. + repeatCharTimes := headerCellWidth / separatorCellWidth + separator := make([]rune, repeatCharTimes) + for i := 0; i < repeatCharTimes; i++ { + separator[i] = t.HeaderSeparatorRune + } + separators[index] = string(separator) + } + + vals := t.applyWidths(separators, t.widths) + if t.HeaderFormatter != nil { + txt := t.HeaderFormatter(format, vals...) + fmt.Fprint(t.Writer, txt) + } else { + fmt.Fprintf(t.Writer, format, vals...) + } +} + func (t *table) printHeader(format string) { vals := t.applyWidths(t.header, t.widths) if t.HeaderFormatter != nil { diff --git a/table_test.go b/table_test.go index dfb0038..992c9f6 100644 --- a/table_test.go +++ b/table_test.go @@ -176,6 +176,47 @@ bippity boppity } } +func TestTable_WithHeaderSeparatorRow(t *testing.T) { + t.Parallel() + + buf := bytes.Buffer{} + tbl := New("foo", "bar").WithHeaderSeparatorRow('-').WithWriter(&buf).AddRow("fizz", "buzz") + + // Add some rows + tbl.AddRow() + tbl.AddRow("cat") + + // add an entry that contains new lines + tbl.AddRow("bippity", "boppity\nboop") + + // Add a couple more rows + tbl.AddRow("a", "b") + tbl.AddRow("c", "d") + + // and another entry with more new lines + tbl.AddRow("1\n2", "x\ny\nz") + + // check the full table + buf.Reset() + tbl.Print() + expected := `foo bar +--- --- +fizz buzz + +cat +bippity boppity + boop +a b +c d +1 x +2 y + z +` + if diff := cmp.Diff(expected, buf.String()); diff != "" { + t.Fatalf("table mismatch (-expected +got):\n%s\nout=%#v", diff, buf.String()) + } +} + func TestTable_AddRow_WithNewLines(t *testing.T) { t.Parallel()