From ed6073d2c00cbe09f4aaa929837a71bf9d82f4ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Wa=C5=9B?= Date: Sun, 21 Mar 2021 12:00:44 +0100 Subject: [PATCH 1/6] Use pools in formatter --- encode.go | 73 ++++++++++++----- example_test.go | 2 +- fmt.go | 211 +++++++++++++++++++++++++++++++++--------------- fmt_test.go | 9 ++- tblfmt_test.go | 7 +- util.go | 6 ++ 6 files changed, 220 insertions(+), 88 deletions(-) diff --git a/encode.go b/encode.go index e319cb1..2312165 100644 --- a/encode.go +++ b/encode.go @@ -72,6 +72,12 @@ type TableEncoder struct { scanCount int // w is the undelying writer w *bufio.Writer + + // scanBuffer for scanning row into + scanBuffer []interface{} + + // valsBuffer for prefetching a set of rows to calculate max column width + valsBuffer [][]*Value } // NewTableEncoder creates a new table encoder using the provided options. @@ -143,6 +149,7 @@ func (enc *TableEncoder) Encode(w io.Writer) error { if err != nil { return err } + enc.initBuffers() var cmd *exec.Cmd var cmdBuf io.WriteCloser for { @@ -159,7 +166,9 @@ func (enc *TableEncoder) Encode(w io.Writer) error { enc.calcWidth(vals) if enc.minExpandWidth != 0 && enc.tableWidth() >= enc.minExpandWidth { t := *enc - t.formatter = NewEscapeFormatter() + if f, ok := t.formatter.(*EscapeFormatter); ok { + f.Configure(WithHeaderAlign(AlignLeft)) + } exp := ExpandedEncoder{ TableEncoder: t, } @@ -235,6 +244,19 @@ func checkErr(err error, cmd *exec.Cmd) error { return err } +func (enc *TableEncoder) initBuffers() { + // create buffers for scanning rows and prefetching records + bufSize := enc.count + if bufSize == 0 { + bufSize = 100 + } + enc.valsBuffer = make([][]*Value, 0, bufSize) + enc.scanBuffer = make([]interface{}, len(enc.headers)) + for i := 0; i < len(enc.headers); i++ { + enc.scanBuffer[i] = new(interface{}) + } +} + func (enc *TableEncoder) encodeVals(vals [][]*Value) error { rs := enc.rowStyle(enc.lineStyle.Row) // print buffered vals @@ -246,6 +268,11 @@ func (enc *TableEncoder) encodeVals(vals [][]*Value) error { return err } } + for _, v := range vals[i] { + if v != nil { + enc.formatter.Free(v) + } + } } return nil } @@ -272,29 +299,21 @@ func (enc *TableEncoder) EncodeAll(w io.Writer) error { // nextResults reads the next enc.count values, // or all values if enc.count = 0 func (enc *TableEncoder) nextResults() ([][]*Value, error) { - var vals [][]*Value - if enc.count != 0 { - vals = make([][]*Value, 0, enc.count) - } - // set up storage for results - r := make([]interface{}, len(enc.headers)) - for i := 0; i < len(enc.headers); i++ { - r[i] = new(interface{}) - } + enc.valsBuffer = enc.valsBuffer[:0] // read to count (or all) var i int for enc.resultSet.Next() { - v, err := enc.scanAndFormat(r) + v, err := enc.scanAndFormat(enc.scanBuffer) if err != nil { - return vals, err + return enc.valsBuffer, err } - vals, i = append(vals, v), i+1 + enc.valsBuffer, i = append(enc.valsBuffer, v), i+1 // read by batches of enc.count rows if enc.count != 0 && i%enc.count == 0 { break } } - return vals, nil + return enc.valsBuffer, nil } func (enc *TableEncoder) calcWidth(vals [][]*Value) { @@ -401,7 +420,7 @@ func (enc *TableEncoder) divider(rs rowStyle) { enc.w.Write(rs.left) for i, width := range enc.maxWidths { // column - enc.w.Write(bytes.Repeat(rs.filler, width)) + repeat(enc.w, rs.filler, width) // line feed indicator if rs.hasWrapping && enc.border >= 1 { enc.w.Write(rs.filler) @@ -507,6 +526,7 @@ func (enc *TableEncoder) row(vals []*Value, rs rowStyle) { } else { if enc.border > 1 || i != len(vals)-1 { enc.w.Write(bytes.Repeat(rs.filler, enc.maxWidths[i])) + repeat(enc.w, rs.filler, enc.maxWidths[i]) } } // write newline wrap value @@ -550,13 +570,13 @@ func (enc *TableEncoder) writeAligned(b, filler []byte, a Align, padding int) { } // add padding left if paddingLeft > 0 { - enc.w.Write(bytes.Repeat(filler, paddingLeft)) + repeat(enc.w, filler, paddingLeft) } // write enc.w.Write(b) // add padding right if paddingRight > 0 { - enc.w.Write(bytes.Repeat(filler, paddingRight)) + repeat(enc.w, filler, paddingRight) } } @@ -602,7 +622,9 @@ func NewExpandedEncoder(resultSet ResultSet, opts ...Option) (Encoder, error) { return nil, err } t := tableEnc.(*TableEncoder) - t.formatter = NewEscapeFormatter() + if f, ok := t.formatter.(*EscapeFormatter); ok { + f.Configure(WithHeaderAlign(AlignLeft)) + } if !t.isCustomSummary { t.summary = nil } @@ -636,6 +658,7 @@ func (enc *ExpandedEncoder) Encode(w io.Writer) error { if err != nil { return err } + enc.initBuffers() var cmd *exec.Cmd var cmdBuf io.WriteCloser wroteTitle := enc.skipHeader @@ -695,6 +718,11 @@ func (enc *ExpandedEncoder) encodeVals(vals [][]*Value) error { return err } } + for _, v := range vals[i] { + if v != nil { + enc.formatter.Free(v) + } + } } // draw end border if enc.border >= 2 && enc.scanCount != 0 { @@ -791,7 +819,7 @@ func (enc *ExpandedEncoder) record(i int, vals []*Value, rs rowStyle) { enc.w.WriteString(header) padding := enc.maxWidths[0] + enc.maxWidths[1] + runewidth.StringWidth(string(headerRS.middle))*2 - len(header) - 1 if padding > 0 { - enc.w.Write(bytes.Repeat(headerRS.filler, padding)) + repeat(enc.w, headerRS.filler, padding) } // write newline wrap value enc.w.Write(headerRS.filler) @@ -933,6 +961,10 @@ func (enc *JSONEncoder) Encode(w io.Writer) error { return err } } + + if v != enc.empty { + enc.formatter.Free(v) + } } if _, err = w.Write(cls); err != nil { return err @@ -1110,6 +1142,9 @@ func (enc *UnalignedEncoder) Encode(w io.Writer) error { if _, err := w.Write(buf); err != nil { return err } + if v != enc.empty { + enc.formatter.Free(v) + } } if _, err := w.Write(enc.newline); err != nil { return err diff --git a/example_test.go b/example_test.go index 5102241..f227ed7 100644 --- a/example_test.go +++ b/example_test.go @@ -91,7 +91,7 @@ func ExampleNewTableEncoder_encodeAll() { res, tblfmt.WithBorder(2), tblfmt.WithLineStyle(tblfmt.UnicodeDoubleLineStyle()), - tblfmt.WithWidths([]int{20, 20}), + tblfmt.WithWidths([]int{20, 20, 20}), tblfmt.WithSummary(tblfmt.DefaultTableSummary()), ) if err != nil { diff --git a/fmt.go b/fmt.go index 5674b8a..9bf34b5 100644 --- a/fmt.go +++ b/fmt.go @@ -6,6 +6,7 @@ import ( "fmt" "strconv" "strings" + "sync" "time" "unicode" "unicode/utf8" @@ -19,6 +20,9 @@ type Formatter interface { Header([]string) ([]*Value, error) // Format returns a slice of formatted value the provided row values. Format([]interface{}) ([]*Value, error) + + // Free the value if it's no longer going to be used + Free(*Value) } // EscapeFormatter is an escaping formatter, that handles formatting the @@ -61,6 +65,8 @@ type EscapeFormatter struct { invalidWidth int // headerAlign is the default header values alignment headerAlign Align + + valuesPool valuesPool } // NewEscapeFormatter creates a escape formatter to handle basic Go values, @@ -73,11 +79,16 @@ func NewEscapeFormatter(opts ...EscapeFormatterOption) *EscapeFormatter { mask: "%d", timeFormat: time.RFC3339Nano, indent: " ", + valuesPool: newValuesPool(), } + f.Configure(opts...) + return f +} + +func (f *EscapeFormatter) Configure(opts ...EscapeFormatterOption) { for _, o := range opts { o(f) } - return f } // Header satisfies the Formatter interface. @@ -92,7 +103,7 @@ func (f *EscapeFormatter) Header(headers []string) ([]*Value, error) { } else if s == "" { s = f.mask } - res[i] = FormatBytes([]byte(s), f.invalid, f.invalidWidth, f.isJSON, f.isRaw, f.sep, f.quote) + res[i] = f.valuesPool.formatBytes([]byte(s), f.invalid, f.invalidWidth, f.isJSON, f.isRaw, f.sep, f.quote) res[i].Align = f.headerAlign } return res, nil @@ -102,131 +113,158 @@ func (f *EscapeFormatter) Header(headers []string) ([]*Value, error) { func (f *EscapeFormatter) Format(vals []interface{}) ([]*Value, error) { n := len(vals) res := make([]*Value, n) - // TODO: change time to v.AppendFormat() + pool + // TODO: use strconv.Format* for numeric times - // TODO: use pool // TODO: allow configurable runes that can be escaped // TODO: handler driver.Valuer for i := 0; i < n; i++ { switch v := (*(vals[i].(*interface{}))).(type) { case nil: case bool: - res[i] = newValue(strconv.FormatBool(v), AlignLeft, false) + res[i] = f.valuesPool.newEmpty(AlignLeft, false) + res[i].Buf = strconv.AppendBool(res[i].Buf, v) case *bool: if v != nil { - res[i] = newValue(strconv.FormatBool(*v), AlignLeft, false) - } - case uint8: - res[i] = &Value{Buf: []byte(string(rune(v))), Width: 1, Align: AlignRight, Raw: true} - case *uint8: - if v != nil { - res[i] = &Value{Buf: []byte(string(rune(*v))), Width: 1, Align: AlignRight, Raw: true} + res[i] = f.valuesPool.newEmpty(AlignLeft, false) + res[i].Buf = strconv.AppendBool(res[i].Buf, *v) } case int: - res[i] = newValue(strconv.FormatInt(int64(v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(v), 10) case int8: - res[i] = newValue(strconv.FormatInt(int64(v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(v), 10) case int16: - res[i] = newValue(strconv.FormatInt(int64(v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(v), 10) case int32: - res[i] = newValue(strconv.FormatInt(int64(v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(v), 10) case int64: - res[i] = newValue(strconv.FormatInt(int64(v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(v), 10) case *int: if v != nil { - res[i] = newValue(strconv.FormatInt(int64(*v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(*v), 10) } case *int8: if v != nil { - res[i] = newValue(strconv.FormatInt(int64(*v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(*v), 10) } case *int16: if v != nil { - res[i] = newValue(strconv.FormatInt(int64(*v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(*v), 10) } case *int32: if v != nil { - res[i] = newValue(strconv.FormatInt(int64(*v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(*v), 10) } case *int64: if v != nil { - res[i] = newValue(strconv.FormatInt(int64(*v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(*v), 10) } case uint: - res[i] = newValue(strconv.FormatInt(int64(v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(v), 10) + case uint8: + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(v), 10) case uint16: - res[i] = newValue(strconv.FormatInt(int64(v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(v), 10) case uint32: - res[i] = newValue(strconv.FormatInt(int64(v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(v), 10) case uint64: - res[i] = newValue(strconv.FormatInt(int64(v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(v), 10) case *uint: if v != nil { - res[i] = newValue(strconv.FormatInt(int64(*v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(*v), 10) + } + case *uint8: + if v != nil { + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(*v), 10) } case *uint16: if v != nil { - res[i] = newValue(strconv.FormatInt(int64(*v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(*v), 10) } case *uint32: if v != nil { - res[i] = newValue(strconv.FormatInt(int64(*v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendInt(res[i].Buf, int64(*v), 10) } case *uint64: if v != nil { // int64 cannot hold uint64's max value - res[i] = newValue(strconv.FormatUint(uint64(*v), 10), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendUint(res[i].Buf, uint64(*v), 10) } case uintptr: - res[i] = newValue(fmt.Sprintf("(0x%x)", v), AlignRight, true) + res[i] = f.valuesPool.newValue(fmt.Sprintf("(0x%x)", v), AlignRight, true) case *uintptr: if v != nil { - res[i] = newValue(fmt.Sprintf("(0x%x)", v), AlignRight, true) + res[i] = f.valuesPool.newValue(fmt.Sprintf("(0x%x)", v), AlignRight, true) } case float32: - res[i] = newValue(strconv.FormatFloat(float64(v), 'g', -1, 32), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendFloat(res[i].Buf, float64(v), 'g', -1, 32) case float64: - res[i] = newValue(strconv.FormatFloat(v, 'g', -1, 64), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendFloat(res[i].Buf, float64(v), 'g', -1, 64) case *float32: if v != nil { - res[i] = newValue(strconv.FormatFloat(float64(*v), 'g', -1, 32), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendFloat(res[i].Buf, float64(*v), 'g', -1, 32) } case *float64: if v != nil { - res[i] = newValue(strconv.FormatFloat(*v, 'g', -1, 64), AlignRight, true) + res[i] = f.valuesPool.newEmpty(AlignRight, true) + res[i].Buf = strconv.AppendFloat(res[i].Buf, float64(*v), 'g', -1, 64) } case complex64: - res[i] = newValue(fmt.Sprintf("%g", v), AlignRight, false) + res[i] = f.valuesPool.newValue(fmt.Sprintf("%g", v), AlignRight, false) case complex128: - res[i] = newValue(fmt.Sprintf("%g", v), AlignRight, false) + res[i] = f.valuesPool.newValue(fmt.Sprintf("%g", v), AlignRight, false) case *complex64: if v != nil { - res[i] = newValue(fmt.Sprintf("%g", *v), AlignRight, false) + res[i] = f.valuesPool.newValue(fmt.Sprintf("%g", *v), AlignRight, false) } case *complex128: if v != nil { - res[i] = newValue(fmt.Sprintf("%g", *v), AlignRight, false) + res[i] = f.valuesPool.newValue(fmt.Sprintf("%g", *v), AlignRight, false) } case []byte: - res[i] = FormatBytes(v, f.invalid, f.invalidWidth, f.isJSON, f.isRaw, f.sep, f.quote) + res[i] = f.valuesPool.formatBytes(v, f.invalid, f.invalidWidth, f.isJSON, f.isRaw, f.sep, f.quote) case *[]byte: if v != nil { - res[i] = FormatBytes(*v, f.invalid, f.invalidWidth, f.isJSON, f.isRaw, f.sep, f.quote) + res[i] = f.valuesPool.formatBytes(*v, f.invalid, f.invalidWidth, f.isJSON, f.isRaw, f.sep, f.quote) } case string: - res[i] = FormatBytes([]byte(v), f.invalid, f.invalidWidth, f.isJSON, f.isRaw, f.sep, f.quote) + res[i] = f.valuesPool.formatBytes([]byte(v), f.invalid, f.invalidWidth, f.isJSON, f.isRaw, f.sep, f.quote) case *string: if v != nil { - res[i] = FormatBytes([]byte(*v), f.invalid, f.invalidWidth, f.isJSON, f.isRaw, f.sep, f.quote) + res[i] = f.valuesPool.formatBytes([]byte(*v), f.invalid, f.invalidWidth, f.isJSON, f.isRaw, f.sep, f.quote) } case time.Time: - res[i] = newValue(v.Format(f.timeFormat), AlignLeft, false) + res[i] = f.valuesPool.newEmpty(AlignLeft, false) + res[i].Buf = v.AppendFormat(res[i].Buf, f.timeFormat) case *time.Time: if v != nil { - res[i] = newValue(v.Format(f.timeFormat), AlignLeft, false) + res[i] = f.valuesPool.newEmpty(AlignLeft, false) + res[i].Buf = v.AppendFormat(res[i].Buf, f.timeFormat) } case fmt.Stringer: - res[i] = FormatBytes([]byte(v.String()), f.invalid, f.invalidWidth, f.isJSON, f.isRaw, f.sep, f.quote) + res[i] = f.valuesPool.formatBytes([]byte(v.String()), f.invalid, f.invalidWidth, f.isJSON, f.isRaw, f.sep, f.quote) default: // TODO: pool if f.marshaler != nil { @@ -234,10 +272,7 @@ func (f *EscapeFormatter) Format(vals []interface{}) ([]*Value, error) { if err != nil { return nil, err } - res[i] = &Value{ - Buf: buf, - Raw: true, - } + res[i] = f.valuesPool.newRaw(buf) } else { // json encode buf := new(bytes.Buffer) @@ -248,33 +283,71 @@ func (f *EscapeFormatter) Format(vals []interface{}) ([]*Value, error) { return nil, err } if f.isJSON { - res[i] = &Value{ - Buf: bytes.TrimSpace(buf.Bytes()), - Raw: true, - } + res[i] = f.valuesPool.newRaw(bytes.TrimSpace(buf.Bytes())) } else { - res[i] = FormatBytes(bytes.TrimSpace(buf.Bytes()), f.invalid, f.invalidWidth, false, f.isRaw, f.sep, f.quote) + res[i] = f.valuesPool.formatBytes(bytes.TrimSpace(buf.Bytes()), f.invalid, f.invalidWidth, false, f.isRaw, f.sep, f.quote) res[i].Raw = true } } } + if res[i] != nil && res[i].Width == 0 { + res[i].Width = len(res[i].Buf) + } } return res, nil } -// valueFromBuffer returns a value from a buffer known not to contain -// characters to escape. -func newValue(str string, align Align, raw bool) *Value { - v := &Value{Buf: []byte(str), Align: align, Raw: raw} +func (f *EscapeFormatter) Free(v *Value) { + f.valuesPool.free(v) +} + +type valuesPool struct { + sync.Pool +} + +func newValuesPool() valuesPool { + return valuesPool{ + Pool: sync.Pool{ + New: func() interface{} { + return new(Value) + }, + }, + } +} + +func (p *valuesPool) newEmpty(align Align, raw bool) *Value { + v := p.Get().(*Value) + v.Align = align + v.Raw = raw + return v +} + +func (p *valuesPool) newValue(str string, align Align, raw bool) *Value { + v := p.Get().(*Value) + v.Buf = append(v.Buf, str...) + v.Align = align + v.Raw = raw + v.Width = len(v.Buf) + return v +} + +func (p *valuesPool) newRaw(b []byte) *Value { + v := p.Get().(*Value) + v.Buf = append(v.Buf, b...) + v.Raw = true v.Width = len(v.Buf) return v } -// FormatBytes parses src, saving escaped (encoded) and unescaped runes to a +// formatBytes parses src, saving escaped (encoded) and unescaped runes to a // Value, along with tab and newline positions in the generated buf. -func FormatBytes(src []byte, invalid []byte, invalidWidth int, isJSON, isCSV bool, sep, quote rune) *Value { - res := &Value{ - Tabs: make([][][2]int, 1), +func (p *valuesPool) formatBytes(src []byte, invalid []byte, invalidWidth int, isJSON, isCSV bool, sep, quote rune) *Value { + res := p.Get().(*Value) + if res.Tabs != nil { + // Tabs slice should always be zeroed + res.Tabs = append(res.Tabs, nil) + } else { + res.Tabs = make([][][2]int, 1) } var tmp [4]byte var r rune @@ -403,6 +476,16 @@ func FormatBytes(src []byte, invalid []byte, invalidWidth int, isJSON, isCSV boo return res } +func (p *valuesPool) free(v *Value) { + v.Buf = v.Buf[:0] + v.Newlines = v.Newlines[:0] + v.Tabs = v.Tabs[:0] + v.Width = 0 + v.Align = AlignLeft + v.Raw = false + p.Pool.Put(v) +} + // Value contains information pertaining to a formatted value. type Value struct { // Buf is the formatted value. diff --git a/fmt_test.go b/fmt_test.go index 8ea619f..0937d31 100644 --- a/fmt_test.go +++ b/fmt_test.go @@ -78,8 +78,9 @@ func TestFormatBytesTabs(t *testing.T) { v("\u8888\t\u8888", 4), v(" \u8888 \t \u8888 ", 8), } + vp := newValuesPool() for i, test := range tests { - v := FormatBytes([]byte(test.s), nil, 0, false, false, 0, 0) + v := vp.formatBytes([]byte(test.s), nil, 0, false, false, 0, 0) if !reflect.DeepEqual(v, test.exp) { t.Errorf( "test %d %q expected %v, got: %v", @@ -109,7 +110,8 @@ func TestFormatBytesComplex(t *testing.T) { "2013": "Boels-Dolmans Cycling Team", "2015": "Boels-Dolmans" }` - v := FormatBytes([]byte(s), nil, 0, false, false, 0, 0) + vp := newValuesPool() + v := vp.formatBytes([]byte(s), nil, 0, false, false, 0, 0) if w := v.MaxWidth(0, 8); w != 39 { t.Errorf("expected width of 39, got: %d", w) } @@ -132,7 +134,8 @@ func TestFormatBytesCSV(t *testing.T) { {",\t\"", "\",\t\"\"\""}, } for i, test := range tests { - v := FormatBytes([]byte(test.s), nil, 0, false, true, ',', '"') + vp := newValuesPool() + v := vp.formatBytes([]byte(test.s), nil, 0, false, true, ',', '"') buf := v.Buf if v.Quoted { buf = append([]byte{'"'}, append(buf, '"')...) diff --git a/tblfmt_test.go b/tblfmt_test.go index ef5311b..68e265a 100644 --- a/tblfmt_test.go +++ b/tblfmt_test.go @@ -270,8 +270,13 @@ func BenchmarkEncodeFormats(b *testing.B) { opts []Option }{ {"aligned", NewTableEncoder, nil}, + {"aligned-batch10", NewTableEncoder, []Option{WithCount(10)}}, + {"aligned-batch100", NewTableEncoder, []Option{WithCount(100)}}, {"json", NewJSONEncoder, nil}, - {"csv", NewUnalignedEncoder, nil}, + {"unaligned", NewUnalignedEncoder, nil}, + {"csv", NewCSVEncoder, nil}, + {"template-asciidoc", NewTemplateEncoder, []Option{WithTemplate("asciidoc")}}, + {"template-html", NewTemplateEncoder, []Option{WithTemplate("html")}}, } for _, enc := range encoders { b.Run(enc.name, func(b *testing.B) { diff --git a/util.go b/util.go index 7f82cb6..0686f20 100644 --- a/util.go +++ b/util.go @@ -108,3 +108,9 @@ func condWrite(w io.Writer, repeat int, runes ...rune) error { return err } */ + +func repeat(w io.Writer, b []byte, c int) { + for i := 0; i < c; i++ { + w.Write(b) + } +} From 268ec57c8340eb18a02e871c94d5f9d7eb51d3ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Wa=C5=9B?= Date: Sun, 21 Mar 2021 20:08:10 +0100 Subject: [PATCH 2/6] Repeat with fewer writes --- encode.go | 25 ++++++++++++------------- util.go | 13 ++++++++++--- util_test.go | 18 ++++++++++++++++++ 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/encode.go b/encode.go index 2312165..2bf3026 100644 --- a/encode.go +++ b/encode.go @@ -349,7 +349,7 @@ func (enc *TableEncoder) header() { rs := enc.rowStyle(enc.lineStyle.Row) 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, maxWidth-enc.title.Width) + enc.writeAligned(enc.title.Buf, &rs, AlignRight, maxWidth-enc.title.Width) enc.w.Write(enc.newline) } // draw top border @@ -397,7 +397,7 @@ func (enc TableEncoder) rowStyle(r [4]rune) rowStyle { wrapper: []byte(string(enc.lineStyle.Wrap[1])), middle: []byte(middle), right: []byte(right + string(enc.newline)), - filler: []byte(filler), + filler: bytes.Repeat([]byte(filler), 8), hasWrapping: runewidth.RuneWidth(enc.lineStyle.Row[1]) > 0, } } @@ -420,10 +420,10 @@ func (enc *TableEncoder) divider(rs rowStyle) { enc.w.Write(rs.left) for i, width := range enc.maxWidths { // column - repeat(enc.w, rs.filler, width) + rs.filler = repeat(enc.w, rs.filler, width) // line feed indicator if rs.hasWrapping && enc.border >= 1 { - enc.w.Write(rs.filler) + enc.w.Write(rs.filler[:1]) } // middle separator if i != len(enc.maxWidths)-1 { @@ -522,11 +522,10 @@ func (enc *TableEncoder) row(vals []*Value, rs rowStyle) { 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) + enc.writeAligned(v.Buf[start:end], &rs, v.Align, padding) } else { if enc.border > 1 || i != len(vals)-1 { - enc.w.Write(bytes.Repeat(rs.filler, enc.maxWidths[i])) - repeat(enc.w, rs.filler, enc.maxWidths[i]) + rs.filler = repeat(enc.w, rs.filler, enc.maxWidths[i]) } } // write newline wrap value @@ -534,7 +533,7 @@ func (enc *TableEncoder) row(vals []*Value, rs rowStyle) { if l < len(v.Newlines) { enc.w.Write(rs.wrapper) } else { - enc.w.Write(rs.filler) + enc.w.Write(rs.filler[:1]) } } remaining = remaining || l < len(v.Newlines) @@ -553,7 +552,7 @@ func (enc *TableEncoder) row(vals []*Value, rs rowStyle) { } } -func (enc *TableEncoder) writeAligned(b, filler []byte, a Align, padding int) { +func (enc *TableEncoder) writeAligned(b []byte, rs *rowStyle, a Align, padding int) { // calc padding paddingLeft := 0 paddingRight := 0 @@ -570,13 +569,13 @@ func (enc *TableEncoder) writeAligned(b, filler []byte, a Align, padding int) { } // add padding left if paddingLeft > 0 { - repeat(enc.w, filler, paddingLeft) + rs.filler = repeat(enc.w, rs.filler, paddingLeft) } // write enc.w.Write(b) // add padding right if paddingRight > 0 { - repeat(enc.w, filler, paddingRight) + rs.filler = repeat(enc.w, rs.filler, paddingRight) } } @@ -819,10 +818,10 @@ func (enc *ExpandedEncoder) record(i int, vals []*Value, rs rowStyle) { enc.w.WriteString(header) padding := enc.maxWidths[0] + enc.maxWidths[1] + runewidth.StringWidth(string(headerRS.middle))*2 - len(header) - 1 if padding > 0 { - repeat(enc.w, headerRS.filler, padding) + headerRS.filler = repeat(enc.w, headerRS.filler, padding) } // write newline wrap value - enc.w.Write(headerRS.filler) + enc.w.Write(headerRS.filler[:1]) enc.w.Write(headerRS.right) } // write each value with column name in first col diff --git a/util.go b/util.go index 0686f20..8d9362a 100644 --- a/util.go +++ b/util.go @@ -109,8 +109,15 @@ func condWrite(w io.Writer, repeat int, runes ...rune) error { } */ -func repeat(w io.Writer, b []byte, c int) { - for i := 0; i < c; i++ { - w.Write(b) +// repeat b c times and write it to w, returning b with increased capacity +func repeat(w io.Writer, b []byte, c int) []byte { + if len(b) >= c { + w.Write(b[:c]) + return b } + for len(b) < c { + b = append(b, b...) + } + w.Write(b[:c]) + return b } diff --git a/util_test.go b/util_test.go index 3e83042..a76e402 100644 --- a/util_test.go +++ b/util_test.go @@ -12,6 +12,7 @@ import ( "os/exec" "strconv" "strings" + "testing" "time" "unicode" ) @@ -338,3 +339,20 @@ type noopWriter struct{} func (*noopWriter) Write(buf []byte) (int, error) { return len(buf), nil } + +func TestRepeat(t *testing.T) { + buf := new(bytes.Buffer) + filler := []byte("x") + filler = repeat(buf, filler, 10) + filler = repeat(buf, filler, 10) + if cap(filler) != 16 { + t.Errorf("Expected filler to have cap of 16, got %d", cap(filler)) + } + if len(filler) != 1 { + t.Errorf("Expected filler to have len of 1, got %d", len(filler)) + } + actual := buf.String() + if actual != "xxxxxxxxxxxxxxxxxxxx" { + t.Errorf("Expected buf to be 20 x's, got %s", actual) + } +} From 3294773faab00b4ac7b0ac2870ceff02ce881201 Mon Sep 17 00:00:00 2001 From: Jan Was Date: Sun, 28 Mar 2021 09:31:59 +0000 Subject: [PATCH 3/6] Reuse buffer for default encoder --- fmt.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/fmt.go b/fmt.go index 9bf34b5..f247f50 100644 --- a/fmt.go +++ b/fmt.go @@ -43,7 +43,9 @@ type EscapeFormatter struct { // []interface{} types. // // If nil, the standard encoding/json.Encoder will be used instead. - marshaler func(interface{}) ([]byte, error) + marshaler func(interface{}) ([]byte, error) + defaultMarshaler *json.Encoder + defaultMarshalerBuffer *bytes.Buffer // prefix is indent prefix used by the JSON encoder when Marshaler is nil. prefix string // indent is the indent used by the JSON encoder when Marshaler is nil. @@ -76,11 +78,15 @@ type EscapeFormatter struct { // values. func NewEscapeFormatter(opts ...EscapeFormatterOption) *EscapeFormatter { f := &EscapeFormatter{ - mask: "%d", - timeFormat: time.RFC3339Nano, - indent: " ", - valuesPool: newValuesPool(), + mask: "%d", + timeFormat: time.RFC3339Nano, + indent: " ", + valuesPool: newValuesPool(), + defaultMarshalerBuffer: new(bytes.Buffer), } + f.defaultMarshaler = json.NewEncoder(f.defaultMarshalerBuffer) + f.defaultMarshaler.SetIndent(f.prefix, f.indent) + f.defaultMarshaler.SetEscapeHTML(f.escapeHTML) f.Configure(opts...) return f } @@ -275,17 +281,14 @@ func (f *EscapeFormatter) Format(vals []interface{}) ([]*Value, error) { res[i] = f.valuesPool.newRaw(buf) } else { // json encode - buf := new(bytes.Buffer) - enc := json.NewEncoder(buf) - enc.SetIndent(f.prefix, f.indent) - enc.SetEscapeHTML(f.escapeHTML) - if err := enc.Encode(v); err != nil { + f.defaultMarshalerBuffer.Reset() + if err := f.defaultMarshaler.Encode(v); err != nil { return nil, err } if f.isJSON { - res[i] = f.valuesPool.newRaw(bytes.TrimSpace(buf.Bytes())) + res[i] = f.valuesPool.newRaw(bytes.TrimSpace(f.defaultMarshalerBuffer.Bytes())) } else { - res[i] = f.valuesPool.formatBytes(bytes.TrimSpace(buf.Bytes()), f.invalid, f.invalidWidth, false, f.isRaw, f.sep, f.quote) + res[i] = f.valuesPool.formatBytes(bytes.TrimSpace(f.defaultMarshalerBuffer.Bytes()), f.invalid, f.invalidWidth, false, f.isRaw, f.sep, f.quote) res[i].Raw = true } } From e5ca7d3d4b1f392a7966e131453f887b3a5f0f17 Mon Sep 17 00:00:00 2001 From: Jan Was Date: Sun, 28 Mar 2021 09:43:01 +0000 Subject: [PATCH 4/6] Reuse rowStyles --- encode.go | 39 +++++++++++++++++++++++++-------------- tblfmt_test.go | 5 +++++ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/encode.go b/encode.go index 2bf3026..d4d9143 100644 --- a/encode.go +++ b/encode.go @@ -35,6 +35,8 @@ type TableEncoder struct { inline bool // lineStyle is the table line style. lineStyle LineStyle + // rowStyles is the table of row style. + rowStyles rowStyles // formatter handles formatting values prior to output. formatter Formatter // skipHeader allows to skip drawing header @@ -100,6 +102,11 @@ func NewTableEncoder(resultSet ResultSet, opts ...Option) (Encoder, error) { return nil, err } } + enc.rowStyles.Top = enc.lineToRowStyle(enc.lineStyle.Top) + enc.rowStyles.Mid = enc.lineToRowStyle(enc.lineStyle.Mid) + enc.rowStyles.Row = enc.lineToRowStyle(enc.lineStyle.Row) + enc.rowStyles.Wrap = enc.lineToRowStyle(enc.lineStyle.Wrap) + enc.rowStyles.End = enc.lineToRowStyle(enc.lineStyle.End) // check linestyle runes // TODO: this check should be removed for _, l := range [][4]rune{ @@ -208,7 +215,7 @@ func (enc *TableEncoder) Encode(w io.Writer) error { } // draw end border if enc.border >= 2 { - enc.divider(enc.rowStyle(enc.lineStyle.End)) + enc.divider(enc.rowStyles.End) } } // add summary @@ -258,7 +265,7 @@ func (enc *TableEncoder) initBuffers() { } func (enc *TableEncoder) encodeVals(vals [][]*Value) error { - rs := enc.rowStyle(enc.lineStyle.Row) + rs := enc.rowStyles.Row // print buffered vals for i := 0; i < len(vals); i++ { enc.row(vals[i], rs) @@ -319,7 +326,7 @@ func (enc *TableEncoder) nextResults() ([][]*Value, error) { func (enc *TableEncoder) calcWidth(vals [][]*Value) { // calc offsets and widths for this batch of rows var offset int - rs := enc.rowStyle(enc.lineStyle.Row) + rs := enc.rowStyles.Row offset += runewidth.StringWidth(string(rs.left)) for i, h := range enc.headers { if i != 0 { @@ -346,7 +353,7 @@ func (enc *TableEncoder) calcWidth(vals [][]*Value) { } func (enc *TableEncoder) header() { - rs := enc.rowStyle(enc.lineStyle.Row) + rs := enc.rowStyles.Row if enc.title != nil && enc.title.Width != 0 { maxWidth := ((enc.tableWidth() - enc.title.Width) / 2) + enc.title.Width enc.writeAligned(enc.title.Buf, &rs, AlignRight, maxWidth-enc.title.Width) @@ -354,24 +361,24 @@ func (enc *TableEncoder) header() { } // draw top border if enc.border >= 2 && !enc.inline { - enc.divider(enc.rowStyle(enc.lineStyle.Top)) + enc.divider(enc.rowStyles.Top) } // draw the header row with top border style if enc.inline { - rs = enc.rowStyle(enc.lineStyle.Top) + rs = enc.rowStyles.Top } // write header enc.row(enc.headers, rs) if !enc.inline { // draw mid divider - enc.divider(enc.rowStyle(enc.lineStyle.Mid)) + enc.divider(enc.rowStyles.Mid) } } // rowStyle returns the left, right and midle borders. // It also profides the filler string, and indicates // if this style uses a wrapping indicator. -func (enc TableEncoder) rowStyle(r [4]rune) rowStyle { +func (enc TableEncoder) lineToRowStyle(r [4]rune) rowStyle { var left, right, middle, spacer, filler string spacer = strings.Repeat(string(r[1]), runewidth.RuneWidth(enc.lineStyle.Row[1])) filler = string(r[1]) @@ -436,7 +443,7 @@ func (enc *TableEncoder) divider(rs rowStyle) { // tableWidth calculates total table width. func (enc *TableEncoder) tableWidth() int { - rs := enc.rowStyle(enc.lineStyle.Mid) + rs := enc.rowStyles.Mid width := runewidth.StringWidth(string(rs.left)) + runewidth.StringWidth(string(rs.right)) for i, w := range enc.maxWidths { width += w @@ -609,6 +616,10 @@ type rowStyle struct { hasWrapping bool } +type rowStyles struct { + Top, Mid, Row, Wrap, End rowStyle +} + // ExpandedEncoder is a buffered, lookahead expanded table encoder for result sets. type ExpandedEncoder struct { TableEncoder @@ -707,7 +718,7 @@ func (enc *ExpandedEncoder) Encode(w io.Writer) error { } func (enc *ExpandedEncoder) encodeVals(vals [][]*Value) error { - rs := enc.rowStyle(enc.lineStyle.Row) + rs := enc.rowStyles.Row // print buffered vals for i := 0; i < len(vals); i++ { enc.record(i, vals[i], rs) @@ -725,7 +736,7 @@ func (enc *ExpandedEncoder) encodeVals(vals [][]*Value) error { } // draw end border if enc.border >= 2 && enc.scanCount != 0 { - enc.divider(enc.rowStyle(enc.lineStyle.End)) + enc.divider(enc.rowStyles.End) } return nil } @@ -750,7 +761,7 @@ func (enc *ExpandedEncoder) EncodeAll(w io.Writer) error { } func (enc *ExpandedEncoder) calcWidth(vals [][]*Value) { - rs := enc.rowStyle(enc.lineStyle.Row) + rs := enc.rowStyles.Row offset := runewidth.StringWidth(string(rs.left)) enc.offsets[0] = offset // first column is always the column name @@ -809,9 +820,9 @@ func (enc *ExpandedEncoder) record(i int, vals []*Value, rs rowStyle) { headerRS := rs header := enc.recordHeader(i) if enc.border != 0 { - headerRS = enc.rowStyle(enc.lineStyle.Top) + headerRS = enc.rowStyles.Top if i != 0 { - headerRS = enc.rowStyle(enc.lineStyle.Mid) + headerRS = enc.rowStyles.Mid } } enc.w.Write(headerRS.left) diff --git a/tblfmt_test.go b/tblfmt_test.go index 68e265a..acb366c 100644 --- a/tblfmt_test.go +++ b/tblfmt_test.go @@ -2,7 +2,9 @@ package tblfmt import ( "bytes" + "encoding/csv" "fmt" + "io" "io/ioutil" "os" "regexp" @@ -269,6 +271,9 @@ func BenchmarkEncodeFormats(b *testing.B) { f Builder opts []Option }{ + {"unaligned", NewCSVEncoder, []Option{WithNewCSVWriter(func(w io.Writer) CSVWriter { + return csv.NewWriter(w) + })}}, {"aligned", NewTableEncoder, nil}, {"aligned-batch10", NewTableEncoder, []Option{WithCount(10)}}, {"aligned-batch100", NewTableEncoder, []Option{WithCount(100)}}, From ec8fe9404b3a589fa2fe92835c0d59335f1b94b2 Mon Sep 17 00:00:00 2001 From: Jan Was Date: Sun, 28 Mar 2021 10:44:37 +0000 Subject: [PATCH 5/6] Reuse value slices in formatter --- fmt.go | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/fmt.go b/fmt.go index f247f50..be5e7c3 100644 --- a/fmt.go +++ b/fmt.go @@ -21,6 +21,12 @@ type Formatter interface { // Format returns a slice of formatted value the provided row values. Format([]interface{}) ([]*Value, error) + // HeaderInto populates a slice with formatted values for the provided headers. + HeaderInto([]string, []*Value) error + + // FormatInto populates a slice with formatted value the provided row values. + FormatInto([]interface{}, []*Value) error + // Free the value if it's no longer going to be used Free(*Value) } @@ -99,8 +105,14 @@ func (f *EscapeFormatter) Configure(opts ...EscapeFormatterOption) { // Header satisfies the Formatter interface. func (f *EscapeFormatter) Header(headers []string) ([]*Value, error) { + res := make([]*Value, len(headers)) + err := f.HeaderInto(headers, res) + return res, err +} + +// HeaderInto satisfies the Formatter interface. +func (f *EscapeFormatter) HeaderInto(headers []string, res []*Value) error { n := len(headers) - res := make([]*Value, n) useMask := strings.Contains(f.mask, "%") for i := 0; i < n; i++ { s := strings.TrimSpace(headers[i]) @@ -112,13 +124,19 @@ func (f *EscapeFormatter) Header(headers []string) ([]*Value, error) { res[i] = f.valuesPool.formatBytes([]byte(s), f.invalid, f.invalidWidth, f.isJSON, f.isRaw, f.sep, f.quote) res[i].Align = f.headerAlign } - return res, nil + return nil } // Format satisfies the Formatter interface. func (f *EscapeFormatter) Format(vals []interface{}) ([]*Value, error) { + res := make([]*Value, len(vals)) + err := f.FormatInto(vals, res) + return res, err +} + +// Format satisfies the Formatter interface. +func (f *EscapeFormatter) FormatInto(vals []interface{}, res []*Value) error { n := len(vals) - res := make([]*Value, n) // TODO: use strconv.Format* for numeric times // TODO: allow configurable runes that can be escaped @@ -276,14 +294,14 @@ func (f *EscapeFormatter) Format(vals []interface{}) ([]*Value, error) { if f.marshaler != nil { buf, err := f.marshaler(v) if err != nil { - return nil, err + return err } res[i] = f.valuesPool.newRaw(buf) } else { // json encode f.defaultMarshalerBuffer.Reset() if err := f.defaultMarshaler.Encode(v); err != nil { - return nil, err + return err } if f.isJSON { res[i] = f.valuesPool.newRaw(bytes.TrimSpace(f.defaultMarshalerBuffer.Bytes())) @@ -297,7 +315,7 @@ func (f *EscapeFormatter) Format(vals []interface{}) ([]*Value, error) { res[i].Width = len(res[i].Buf) } } - return res, nil + return nil } func (f *EscapeFormatter) Free(v *Value) { From 21cf5ea0555663140cab51a8f2fa8af55b011703 Mon Sep 17 00:00:00 2001 From: Jan Was Date: Sun, 28 Mar 2021 12:00:28 +0000 Subject: [PATCH 6/6] Treat filler as runes, not bytes --- encode.go | 38 ++++++++++++++++++++------------------ example_test.go | 2 +- tblfmt_test.go | 5 ----- util_test.go | 7 ++----- 4 files changed, 23 insertions(+), 29 deletions(-) diff --git a/encode.go b/encode.go index d4d9143..5f4f819 100644 --- a/encode.go +++ b/encode.go @@ -215,7 +215,7 @@ func (enc *TableEncoder) Encode(w io.Writer) error { } // draw end border if enc.border >= 2 { - enc.divider(enc.rowStyles.End) + enc.divider(&enc.rowStyles.End) } } // add summary @@ -268,7 +268,7 @@ func (enc *TableEncoder) encodeVals(vals [][]*Value) error { rs := enc.rowStyles.Row // print buffered vals for i := 0; i < len(vals); i++ { - enc.row(vals[i], rs) + enc.row(vals[i], &rs) if i+1%1000 == 0 { // check error every 1k rows if err := enc.w.Flush(); err != nil { @@ -361,17 +361,17 @@ func (enc *TableEncoder) header() { } // draw top border if enc.border >= 2 && !enc.inline { - enc.divider(enc.rowStyles.Top) + enc.divider(&enc.rowStyles.Top) } // draw the header row with top border style if enc.inline { rs = enc.rowStyles.Top } // write header - enc.row(enc.headers, rs) + enc.row(enc.headers, &rs) if !enc.inline { // draw mid divider - enc.divider(enc.rowStyles.Mid) + enc.divider(&enc.rowStyles.Mid) } } @@ -405,6 +405,7 @@ func (enc TableEncoder) lineToRowStyle(r [4]rune) rowStyle { middle: []byte(middle), right: []byte(right + string(enc.newline)), filler: bytes.Repeat([]byte(filler), 8), + fillerWidth: len([]byte(filler)), hasWrapping: runewidth.RuneWidth(enc.lineStyle.Row[1]) > 0, } } @@ -422,15 +423,15 @@ func (enc *TableEncoder) scanAndFormat(vals []interface{}) ([]*Value, error) { } // divider draws a divider. -func (enc *TableEncoder) divider(rs rowStyle) { +func (enc *TableEncoder) divider(rs *rowStyle) { // left enc.w.Write(rs.left) for i, width := range enc.maxWidths { // column - rs.filler = repeat(enc.w, rs.filler, width) + rs.filler = repeat(enc.w, rs.filler, rs.fillerWidth*width) // line feed indicator if rs.hasWrapping && enc.border >= 1 { - enc.w.Write(rs.filler[:1]) + enc.w.Write(rs.filler[:rs.fillerWidth]) } // middle separator if i != len(enc.maxWidths)-1 { @@ -497,7 +498,7 @@ func (enc *TableEncoder) tableHeight(rows [][]*Value) int { } // row draws the a table row. -func (enc *TableEncoder) row(vals []*Value, rs rowStyle) { +func (enc *TableEncoder) row(vals []*Value, rs *rowStyle) { var l int for { // left @@ -529,10 +530,10 @@ func (enc *TableEncoder) row(vals []*Value, rs rowStyle) { if enc.border <= 1 && i == len(vals)-1 && (!rs.hasWrapping || l >= len(v.Newlines)) { padding = 0 } - enc.writeAligned(v.Buf[start:end], &rs, v.Align, padding) + enc.writeAligned(v.Buf[start:end], rs, v.Align, padding) } else { if enc.border > 1 || i != len(vals)-1 { - rs.filler = repeat(enc.w, rs.filler, enc.maxWidths[i]) + rs.filler = repeat(enc.w, rs.filler, rs.fillerWidth*enc.maxWidths[i]) } } // write newline wrap value @@ -540,7 +541,7 @@ func (enc *TableEncoder) row(vals []*Value, rs rowStyle) { if l < len(v.Newlines) { enc.w.Write(rs.wrapper) } else { - enc.w.Write(rs.filler[:1]) + enc.w.Write(rs.filler[:rs.fillerWidth]) } } remaining = remaining || l < len(v.Newlines) @@ -576,13 +577,13 @@ func (enc *TableEncoder) writeAligned(b []byte, rs *rowStyle, a Align, padding i } // add padding left if paddingLeft > 0 { - rs.filler = repeat(enc.w, rs.filler, paddingLeft) + rs.filler = repeat(enc.w, rs.filler, rs.fillerWidth*paddingLeft) } // write enc.w.Write(b) // add padding right if paddingRight > 0 { - rs.filler = repeat(enc.w, rs.filler, paddingRight) + rs.filler = repeat(enc.w, rs.filler, rs.fillerWidth*paddingRight) } } @@ -613,6 +614,7 @@ func (enc *TableEncoder) summarize(w io.Writer) error { // rowStyle is the row style for a row, as arrays of bytes to print. type rowStyle struct { left, right, middle, filler, wrapper []byte + fillerWidth int hasWrapping bool } @@ -736,7 +738,7 @@ func (enc *ExpandedEncoder) encodeVals(vals [][]*Value) error { } // draw end border if enc.border >= 2 && enc.scanCount != 0 { - enc.divider(enc.rowStyles.End) + enc.divider(&enc.rowStyles.End) } return nil } @@ -829,10 +831,10 @@ func (enc *ExpandedEncoder) record(i int, vals []*Value, rs rowStyle) { enc.w.WriteString(header) padding := enc.maxWidths[0] + enc.maxWidths[1] + runewidth.StringWidth(string(headerRS.middle))*2 - len(header) - 1 if padding > 0 { - headerRS.filler = repeat(enc.w, headerRS.filler, padding) + headerRS.filler = repeat(enc.w, headerRS.filler, rs.fillerWidth*padding) } // write newline wrap value - enc.w.Write(headerRS.filler[:1]) + enc.w.Write(headerRS.filler[:rs.fillerWidth]) enc.w.Write(headerRS.right) } // write each value with column name in first col @@ -840,7 +842,7 @@ func (enc *ExpandedEncoder) record(i int, vals []*Value, rs rowStyle) { if v != nil { v.Align = AlignLeft } - enc.row([]*Value{enc.headers[j], v}, rs) + enc.row([]*Value{enc.headers[j], v}, &rs) } } diff --git a/example_test.go b/example_test.go index f227ed7..69a87a0 100644 --- a/example_test.go +++ b/example_test.go @@ -91,7 +91,7 @@ func ExampleNewTableEncoder_encodeAll() { res, tblfmt.WithBorder(2), tblfmt.WithLineStyle(tblfmt.UnicodeDoubleLineStyle()), - tblfmt.WithWidths([]int{20, 20, 20}), + tblfmt.WithWidths([]int{20, 20, 1}), tblfmt.WithSummary(tblfmt.DefaultTableSummary()), ) if err != nil { diff --git a/tblfmt_test.go b/tblfmt_test.go index acb366c..68e265a 100644 --- a/tblfmt_test.go +++ b/tblfmt_test.go @@ -2,9 +2,7 @@ package tblfmt import ( "bytes" - "encoding/csv" "fmt" - "io" "io/ioutil" "os" "regexp" @@ -271,9 +269,6 @@ func BenchmarkEncodeFormats(b *testing.B) { f Builder opts []Option }{ - {"unaligned", NewCSVEncoder, []Option{WithNewCSVWriter(func(w io.Writer) CSVWriter { - return csv.NewWriter(w) - })}}, {"aligned", NewTableEncoder, nil}, {"aligned-batch10", NewTableEncoder, []Option{WithCount(10)}}, {"aligned-batch100", NewTableEncoder, []Option{WithCount(100)}}, diff --git a/util_test.go b/util_test.go index a76e402..05daecb 100644 --- a/util_test.go +++ b/util_test.go @@ -345,11 +345,8 @@ func TestRepeat(t *testing.T) { filler := []byte("x") filler = repeat(buf, filler, 10) filler = repeat(buf, filler, 10) - if cap(filler) != 16 { - t.Errorf("Expected filler to have cap of 16, got %d", cap(filler)) - } - if len(filler) != 1 { - t.Errorf("Expected filler to have len of 1, got %d", len(filler)) + if len(filler) != 16 { + t.Errorf("Expected filler to have len of 16, got %d", len(filler)) } actual := buf.String() if actual != "xxxxxxxxxxxxxxxxxxxx" {