Skip to content

Commit

Permalink
Add WithHeaderSeparatorRow (#40)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Bios-Marcel committed Apr 2, 2024
1 parent 3d72bb9 commit a8aa9fe
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 27 deletions.
94 changes: 67 additions & 27 deletions table.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -152,6 +153,7 @@ type table struct {
Padding int
Writer io.Writer
Width WidthFunc
HeaderSeparatorRune rune

header []string
rows [][]string
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
41 changes: 41 additions & 0 deletions table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down

0 comments on commit a8aa9fe

Please sign in to comment.