Documentation
¶
Overview ¶
Package table builds tabular data and renders it as aligned text.
A Table is a set of Columns plus the rows added to them. Cell values are stored as given -- an int stays an int -- and are only turned into text when the table is rendered. That ordering is deliberate: a value formatted on the way in cannot be recovered, so keeping it typed is what leaves room for renderers other than text (CSV first, since it shares this row model) without a change to how tables are built.
The zero value of a Table is not useful; construct one with New.
t := table.New(
table.Column{Header: "offset", Align: table.Right, Format: "%+.2fs"},
table.Column{Header: "score", Align: table.Right, Format: "%.3f"},
table.Column{Header: "note"},
)
t.Append(2.75, 0.366, "the corner")
fmt.Println(t)
Display width, not byte length ¶
Column widths are measured in terminal columns, via Table.Width. The default handles East Asian wide characters, combining marks and emoji correctly; len() and utf8.RuneCountInString both get at least one of those wrong, and the symptom is a table that looks fine until someone's data is not ASCII. See Table.Width to substitute a cheaper measure.
Example ¶
package main
import (
"fmt"
"github.com/wisborg/output/table"
)
func main() {
t := table.New(
table.Column{Header: "clip"},
table.Column{Header: "offset", Align: table.Right, Format: "%+.2fs"},
table.Column{Header: "score", Align: table.Right, Format: "%.3f"},
)
t.MustAppend("corner_1", 2.70, 0.821)
t.MustAppend("corner_3", 2.75, 0.366)
fmt.Print(t)
}
Output: clip offset score ------------------------- corner_1 +2.70s 0.821 corner_3 +2.75s 0.366
Index ¶
- Constants
- type Align
- type CSVStyle
- type Column
- type Style
- type Table
- func (t *Table) Append(cells ...any) error
- func (t *Table) AppendSeparator()
- func (t *Table) MustAppend(cells ...any)
- func (t *Table) Render(w io.Writer, style Style) error
- func (t *Table) Reset()
- func (t *Table) Rows() int
- func (t *Table) String() string
- func (t *Table) WriteCSV(w io.Writer, style CSVStyle) error
Examples ¶
Constants ¶
const DefaultSpacing = 3
DefaultSpacing is the gap, in spaces, between adjacent columns of an unframed table. Three is wide enough that a right-aligned column and the left-aligned one beside it do not read as a single run of text, which two spaces does at small column widths.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Align ¶
type Align int
Align is a column's horizontal alignment. The zero value is Left, so a Column literal that says nothing about alignment gets the conventional default for text.
const ( // Left pads on the right. The zero value, and the right default for // text: ragged right edges are easy to read down. Left Align = iota // Right pads on the left. Use it for numbers, where digits lining up // under each other is the whole point of a table. Right // Center splits the padding, with any odd column going to the right. Center )
type CSVStyle ¶
type CSVStyle struct {
// OmitHeader leaves out the header row. Use it when appending to a file
// that already has one.
OmitHeader bool
// Comma is the field delimiter. 0 means ',' -- so the zero CSVStyle is
// ordinary CSV, and this only has to be set to depart from it (e.g. '\t'
// for TSV, or ';' where a decimal comma is in use).
Comma rune
}
CSVStyle controls CSV output. The zero value writes a header row and comma-separated fields, which is what almost every consumer expects.
type Column ¶
type Column struct {
// Header is the column heading. It participates in the column's width,
// so a heading longer than every value sets that column's width.
Header string
// Align is how cells AND the heading are positioned within the column.
//
// Note the heading follows the column, rather than always being
// left-aligned: over a right-aligned numeric column, a left-aligned
// heading floats away from the digits it names and is measurably
// harder to scan. This is a deliberate difference from some other
// table renderers.
Align Align
// Format is a fmt verb applied to each cell value, such as "%.2f" or
// "%+d" or "%q". Empty means the value is rendered with %v.
//
// This is a fmt verb rather than a bespoke format language because Go
// programmers already know fmt, already have its documentation, and
// get its whole vocabulary -- width, precision, sign, base -- without
// this package reimplementing any of it. A verb that does not match
// the value's type produces fmt's own %!v(...) marker in the cell,
// which is visible in the output rather than silently wrong.
Format string
// MaxWidth truncates a rendered cell to this many display columns.
// 0 (the zero value) means unlimited.
//
// Truncation is by display width, so a wide character that would
// straddle the limit is dropped rather than half-printed. It is hard
// truncation with no ellipsis: an ellipsis would have to be counted
// against MaxWidth, which makes the limit mean two different things
// depending on whether it was hit.
MaxWidth int
}
Column describes one column: its heading and how its cells are turned into text. Every field is safe to change after the Table is built and before it is rendered -- nothing about a column is baked into the rows, because cells are only formatted at render time. Changing Align on a table you have already filled is a supported operation, not a trick:
t.Columns[1].Align = table.Right
type Style ¶
type Style struct {
// Frame draws rules and pipes around every cell, in the style of the
// mysql client:
//
// +--------+-------+
// | offset | score |
// +--------+-------+
// | +2.75s | 0.366 |
// +--------+-------+
Frame bool
// Spacing is the number of spaces between adjacent columns when Frame
// is false. 0 means DefaultSpacing; use a negative value for no gap at
// all, which is otherwise unreachable.
Spacing int
// Multiline splits cell values on "\n" and gives each line its own
// physical row, keeping the columns aligned:
//
// name detail
// ------ --------------
// first line one
// line two
//
// Without it a value containing a newline is written through as-is,
// which breaks the alignment of everything after it. It is off by
// default because detecting newlines costs a scan of every cell and
// most tables have none.
Multiline bool
}
Style controls how a table is drawn. The zero Style is a valid, unframed, single-line table with DefaultSpacing between columns -- so Render(w, table.Style{}) is the plain rendering, and each field turns on one departure from it.
type Table ¶
type Table struct {
// Columns describes the columns. It is exported so a caller can adjust
// a column between building the table and rendering it -- typically
// alignment, once the data has shown what a column actually holds.
//
// Appending to or truncating this slice after rows exist is allowed
// but is unlikely to be what you want: Render pads rows that are too
// short and ignores cells with no column, so the result is a table
// with blank or missing data rather than an error.
Columns []Column
// Width measures a string's width in terminal columns. It defaults to
// runewidth.StringWidth, which is correct for East Asian wide
// characters, combining marks and emoji.
//
// Substitute a cheaper measure when the data is known to be simple and
// the dependency's cost is not wanted -- utf8.RuneCountInString is
// right for ASCII and precomposed Latin text, and len() is right for
// ASCII alone. Both under-measure CJK by half and mis-measure emoji,
// which shows up as a ragged right edge rather than as an error.
//
// Nil is treated as the default rather than panicking, so a Table{}
// assembled without New still renders.
Width func(string) int
// contains filtered or unexported fields
}
Table is a set of columns and the rows added to them. It is not safe for concurrent use; build a Table on one goroutine and render it there, or guard it yourself.
func (*Table) Append ¶
Append adds a row. The number of cells must match the number of columns.
It returns an error rather than panicking or quietly padding, because a wrong cell count is a caller bug that a table would otherwise absorb into plausible-looking output -- a shifted column reads as bad data, not as a mistake in the code that produced it.
func (*Table) AppendSeparator ¶
func (t *Table) AppendSeparator()
AppendSeparator adds a horizontal rule after the rows added so far. Several in a row collapse to one when rendered; a leading or trailing one is dropped, so callers can add separators unconditionally in a loop.
Example ¶
ExampleTable_AppendSeparator groups rows with a horizontal rule. Separators keep the position they were added at, so appending more rows afterwards does not move them.
package main
import (
"fmt"
"github.com/wisborg/output/table"
)
func main() {
t := table.New(
table.Column{Header: "group"},
table.Column{Header: "value", Align: table.Right},
)
t.MustAppend("first", 1)
t.AppendSeparator()
t.MustAppend("second", 22)
t.MustAppend("third", 333)
fmt.Print(t)
}
Output: group value -------------- first 1 -------------- second 22 third 333
func (*Table) MustAppend ¶
MustAppend is Append, panicking on a cell-count mismatch. Use it for rows built from literals in the same function as the New call, where the count is checked by reading the code and an error return is noise.
func (*Table) Render ¶
Render writes the table to w in the given style.
A table with no rows renders as nothing at all -- not a header, not an empty frame. A caller that wants "no results" said out loud should say it themselves, because only they know what the absence means; a bare header over nothing reads as though the data failed to load.
Example (Frame) ¶
package main
import (
"fmt"
"os"
"github.com/wisborg/output/table"
)
func main() {
t := table.New(
table.Column{Header: "a"},
table.Column{Header: "b", Align: table.Right},
)
t.MustAppend("x", 12)
if err := t.Render(os.Stdout, table.Style{Frame: true}); err != nil {
fmt.Println("render:", err)
}
}
Output: +---+----+ | a | b | +---+----+ | x | 12 | +---+----+
Example (Multiline) ¶
package main
import (
"fmt"
"os"
"github.com/wisborg/output/table"
)
func main() {
t := table.New(
table.Column{Header: "name"},
table.Column{Header: "detail"},
)
t.MustAppend("first", "line one\nline two")
t.MustAppend("second", "single")
if err := t.Render(os.Stdout, table.Style{Multiline: true}); err != nil {
fmt.Println("render:", err)
}
}
Output: name detail ----------------- first line one line two second single
func (*Table) Reset ¶
func (t *Table) Reset()
Reset removes every row and separator, keeping the columns. It is for reusing a Table across several renders of the same shape.
func (*Table) String ¶
String renders the table in the default style. It makes *Table satisfy fmt.Stringer, so a table can be passed straight to fmt.Println.
Errors from the underlying write cannot occur against a strings.Builder, so this signature has nothing to report; use Render to write to a real io.Writer and see its error.
func (*Table) WriteCSV ¶
WriteCSV writes the table as CSV.
It shares the Table's rows with the text renderer and differs from it only where the two media genuinely differ:
- Align, MaxWidth and Style have no effect. Padding and truncation are concessions to a fixed-width display; a CSV is data, and silently shortening a value on the way into a file someone will compute with is a good way to lose the value. Column.Format DOES apply, because that is the caller saying how the value should be written, not how wide it may be.
- Separators are skipped. A horizontal rule groups rows for a reader; there is no row in a CSV that could carry one.
- A table with no rows still writes its header, where the text renderer writes nothing at all. The reasons point in opposite directions: a header over blank space misleads a human into thinking data failed to load, while a program reading the CSV usually needs the header line to know the shape of what it got, and a zero-byte file breaks parsers that require one.
Cells containing the delimiter, quotes or newlines are quoted by encoding/csv, so Style.Multiline has no CSV equivalent and needs none.
Example ¶
ExampleTable_WriteCSV renders the same table as data rather than as text. Align and MaxWidth are display concessions and do not apply; Column.Format does, because it says how a value should be written.
package main
import (
"fmt"
"os"
"github.com/wisborg/output/table"
)
func main() {
t := table.New(
table.Column{Header: "clip"},
table.Column{Header: "offset", Align: table.Right, Format: "%+.2f"},
table.Column{Header: "note", MaxWidth: 4},
)
t.MustAppend("corner_1", 2.70, "the corner")
t.AppendSeparator() // no meaning in CSV; skipped
t.MustAppend("corner_3", 2.75, "also, a corner")
if err := t.WriteCSV(os.Stdout, table.CSVStyle{}); err != nil {
fmt.Println("csv:", err)
}
}
Output: clip,offset,note corner_1,+2.70,the corner corner_3,+2.75,"also, a corner"