blob: 1795865472ad5aa80f96ec84ce811e9174011e80 [file] [log] [blame]
// Copyright 2016 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 table
import (
"fmt"
"io"
"os"
"reflect"
"strings"
)
// TODO: Have a format struct with options for things like column
// separator, and header separator. Provide some defaults ones for,
// e.g., Markdown, CSV, TSV, and such. Make top-level Print and Fprint
// call methods in some default format.
// Print(...) is shorthand for Fprint(os.Stderr, ...).
func Print(g Grouping, formats ...string) error {
return Fprint(os.Stdout, g, formats...)
}
// Fprint prints Grouping g to w. formats[i] specifies a fmt-style
// format string for column i. If there are more columns than formats,
// remaining columns are formatted with %v (in particular, formats may
// be omitted entirely to use %v for all columns). Numeric columns are
// right aligned; all other column types are left aligned.
func Fprint(w io.Writer, g Grouping, formats ...string) error {
if g.Columns() == nil {
return nil
}
// Convert each column to strings.
ss := make([][]string, len(g.Columns()))
rowFmts := make([]string, len(g.Columns()))
for i, col := range g.Columns() {
format := "%v"
if i < len(formats) {
format = formats[i]
}
// Format column.
var valKind reflect.Kind
ss[i] = []string{col}
for _, gid := range g.Tables() {
seq := reflect.ValueOf(g.Table(gid).Column(col))
for row := 0; row < seq.Len(); row++ {
str := fmt.Sprintf(format, seq.Index(row).Interface())
ss[i] = append(ss[i], str)
}
if valKind == reflect.Invalid {
valKind = seq.Type().Elem().Kind()
}
}
// Find column width.
width := 0
for _, s := range ss[i] {
if len(s) > width {
width = len(s)
}
}
// If it's a numeric column, right align.
//
// TODO: Even better would be to decimal align, though
// that may require some understanding of the format;
// or we could only do it for the default format.
switch valKind {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64:
width = -width
}
if i == len(g.Columns())-1 && width > 0 {
// Don't pad the last column.
rowFmts[i] = "%s"
} else {
rowFmts[i] = fmt.Sprintf("%%%ds", -width)
}
}
// Compute group headers.
groups := []GroupID{}
groupPos := []int{}
lastPos := 1
for _, gid := range g.Tables() {
groups = append(groups, gid)
groupPos = append(groupPos, lastPos)
lastPos += g.Table(gid).Len()
}
if len(groups) == 1 && groups[0] == RootGroupID {
groups, groupPos = nil, nil
}
// Print rows.
rowFmt := strings.Join(rowFmts, " ") + "\n"
rowBuf := make([]interface{}, len(rowFmts))
for row := 0; row < len(ss[0]); row++ {
if len(groupPos) > 0 && row == groupPos[0] {
_, err := fmt.Fprintf(w, "-- %s\n", groups[0])
if err != nil {
return err
}
groups, groupPos = groups[1:], groupPos[1:]
}
for col := range rowBuf {
rowBuf[col] = ss[col][row]
}
_, err := fmt.Fprintf(w, rowFmt, rowBuf...)
if err != nil {
return err
}
}
return nil
}