Skip to content

Commit

Permalink
Implement unaligned format
Browse files Browse the repository at this point in the history
  • Loading branch information
nineinchnick committed Mar 27, 2021
1 parent 97bbfd9 commit 846f3aa
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 17 deletions.
54 changes: 54 additions & 0 deletions csv/writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package csv

import (
"bufio"
"io"
)

// Writer identical to "encoding/csv", but does not quote values and allows any newline.
type Writer struct {
Comma rune
Newline string
w *bufio.Writer
}

// NewWriter returns a new Writer that writes to w.
func NewWriter(w io.Writer) *Writer {
return &Writer{
Comma: ',',
Newline: "\n",
w: bufio.NewWriter(w),
}
}

// Write writes a single CSV record to w.
// A record is a slice of strings with each string being one field.
// Writes are buffered, so Flush must eventually be called to ensure
// that the record is written to the underlying io.Writer.
func (w *Writer) Write(record []string) error {
for n, field := range record {
if n > 0 {
if _, err := w.w.WriteRune(w.Comma); err != nil {
return err
}
}

if _, err := w.w.WriteString(field); err != nil {
return err
}
}
_, err := w.w.WriteString(w.Newline)
return err
}

// Flush writes any buffered data to the underlying io.Writer.
// To check if an error occurred during the Flush, call Error.
func (w *Writer) Flush() {
w.w.Flush()
}

// Error reports any error that has occurred during a previous Write or Flush.
func (w *Writer) Error() error {
_, err := w.w.Write(nil)
return err
}
47 changes: 37 additions & 10 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ func (enc *TableEncoder) header() {

if enc.title != nil && enc.title.Width != 0 {
maxWidth := ((enc.tableWidth() - enc.title.Width) / 2) + enc.title.Width
enc.writeAligned(enc.title.Buf, rs.filler, AlignRight, enc.title.Width, maxWidth)
enc.writeAligned(enc.title.Buf, rs.filler, AlignRight, maxWidth-enc.title.Width)
enc.w.Write(enc.newline)
}
// draw top border
Expand Down Expand Up @@ -590,9 +590,16 @@ func (enc *TableEncoder) row(vals []*Value, rs rowStyle) {
width += v.Width
}

enc.writeAligned(v.Buf[start:end], rs.filler, v.Align, width, enc.maxWidths[i])
padding := enc.maxWidths[i] - width
// no padding for last cell if no border
if enc.border <= 1 && i == len(vals)-1 && (!rs.hasWrapping || l >= len(v.Newlines)) {
padding = 0
}
enc.writeAligned(v.Buf[start:end], rs.filler, v.Align, padding)
} else {
enc.w.Write(bytes.Repeat(rs.filler, enc.maxWidths[i]))
if enc.border > 1 || i != len(vals)-1 {
enc.w.Write(bytes.Repeat(rs.filler, enc.maxWidths[i]))
}
}

// write newline wrap value
Expand Down Expand Up @@ -624,9 +631,8 @@ func (enc *TableEncoder) row(vals []*Value, rs rowStyle) {
}
}

func (enc *TableEncoder) writeAligned(b, filler []byte, a Align, width, max int) {
func (enc *TableEncoder) writeAligned(b, filler []byte, a Align, padding int) {
// calc padding
padding := max - width
paddingLeft := 0
paddingRight := 0
switch a {
Expand Down Expand Up @@ -1129,12 +1135,18 @@ func (enc *JSONEncoder) scanAndFormat(vals []interface{}) ([]*Value, error) {

// CSVEncoder is an unbuffered CSV encoder for result sets.
type CSVEncoder struct {
// ResultSet is the result set to encode.
// resultSet is the result set to encode.
resultSet ResultSet

// newCSVWriter that should have all options already set
newCSVWriter func(io.Writer) CSVWriter

// fieldsep is the field separator to use.
fieldsep rune

// fieldsep is true if fieldsep should be a zero byte.
fieldsepIsZero bool

// newline is the newline to use.
newline []byte

Expand All @@ -1148,6 +1160,12 @@ type CSVEncoder struct {
empty *Value
}

type CSVWriter interface {
Write([]string) error
Flush()
Error() error
}

// NewCSVEncoder creates a new CSV encoder using the provided options.
func NewCSVEncoder(resultSet ResultSet, opts ...Option) (Encoder, error) {
var err error
Expand All @@ -1164,6 +1182,18 @@ func NewCSVEncoder(resultSet ResultSet, opts ...Option) (Encoder, error) {
return nil, err
}
}
if enc.newCSVWriter == nil {
enc.newCSVWriter = func(w io.Writer) CSVWriter {
writer := csv.NewWriter(w)
if enc.fieldsep != 0 {
writer.Comma = enc.fieldsep
}
if enc.fieldsepIsZero {
writer.Comma = 0
}
return writer
}
}
return enc, nil
}

Expand All @@ -1177,10 +1207,7 @@ func (enc *CSVEncoder) Encode(w io.Writer) error {
var i int
var err error

c := csv.NewWriter(w)
if enc.fieldsep != 0 {
c.Comma = enc.fieldsep
}
c := enc.newCSVWriter(w)

// get and check columns
cols, err := enc.resultSet.Columns()
Expand Down
53 changes: 46 additions & 7 deletions opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
txt "text/template"
"unicode/utf8"

"github.com/xo/tblfmt/csv"

"github.com/nathan-fiscaletti/consolesize-go"
)

Expand All @@ -24,11 +26,39 @@ func FromMap(opts map[string]string) (Builder, []Option) {
case "json":
return NewJSONEncoder, nil

case "csv":
case "csv", "unaligned":
var csvOpts []Option
if s, ok := opts["fieldsep"]; ok {
sep, _ := utf8.DecodeRuneInString(s)
csvOpts = append(csvOpts, WithFieldSeparator(sep))
if opts["format"] == "unaligned" {
newline := "\n"
if s, ok := opts["recordsep"]; ok {
newline = s
}
fieldsep := '|'
if s, ok := opts["fieldsep"]; ok {
r, _ := utf8.DecodeRuneInString(s)
fieldsep = r
}
if s, ok := opts["fieldsep_zero"]; ok && s == "on" {
fieldsep = 0
}
csvOpts = append(csvOpts, WithNewCSVWriter(func(w io.Writer) CSVWriter {
writer := csv.NewWriter(w)
writer.Newline = newline
writer.Comma = fieldsep
return writer
}))
} else {
csvOpts = append(csvOpts, WithNewline(""))
// recognize both for backward-compatibility, but csv_fieldsep takes precedence
for _, name := range []string{"fieldsep", "csv_fieldsep"} {
if s, ok := opts[name]; ok {
sep, _ := utf8.DecodeRuneInString(s)
csvOpts = append(csvOpts, WithFieldSeparator(sep))
}
}
}
if s, ok := opts["fieldsep_zero"]; ok && s == "on" {
csvOpts = append(csvOpts, WithFieldSeparator(0))
}
if s, ok := opts["tuples_only"]; ok && s == "on" {
csvOpts = append(csvOpts, WithSkipHeader(true))
Expand All @@ -42,9 +72,6 @@ func FromMap(opts map[string]string) (Builder, []Option) {
WithTitle(opts["title"]),
}

case "unaligned":
fallthrough

case "aligned":
var tableOpts []Option
if s, ok := opts["border"]; ok {
Expand Down Expand Up @@ -345,12 +372,24 @@ func WithNewline(newline string) Option {
}
}

// WithNewCSVWriter is a encoder option to set the newCSVWriter func.
func WithNewCSVWriter(f func(io.Writer) CSVWriter) Option {
return func(v interface{}) error {
switch enc := v.(type) {
case *CSVEncoder:
enc.newCSVWriter = f
}
return nil
}
}

// WithFieldSeparator is a encoder option to set the field separator.
func WithFieldSeparator(fieldsep rune) Option {
return func(v interface{}) error {
switch enc := v.(type) {
case *CSVEncoder:
enc.fieldsep = fieldsep
enc.fieldsepIsZero = fieldsep == 0
}
return nil
}
Expand Down
3 changes: 3 additions & 0 deletions tblfmt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ func TestEncodeFormats(t *testing.T) {
name: "unaligned",
params: map[string]string{
"format": "unaligned",
"title": "won't print",
// TODO psql does print the footer
"footer": "off",
},
},
{
Expand Down

0 comments on commit 846f3aa

Please sign in to comment.