Skip to content

Commit 83cd1d3

Browse files
committed
table: improve horizontal rendering
We now dynamically choose the width of the columns in horizontal mode. The commit also simplifies some of the internals.
1 parent ecc18e0 commit 83cd1d3

File tree

6 files changed

+125
-134
lines changed

6 files changed

+125
-134
lines changed

internal/ascii/table/examples_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func ExampleDefine() {
2727
table.Int("cuteness", 8, table.AlignRight, func(c Cat) int { return c.Cuteness }),
2828
)
2929

30-
board := ascii.Make(8, tbl.CumulativeFieldWidth)
30+
board := ascii.Make(8, 1)
3131
fmt.Println("Cool cats:")
3232
tbl.Render(board.At(0, 0), table.RenderOptions{}, slices.Values([]Cat{
3333
{Name: "Chicken", Age: 5, Cuteness: 10},
@@ -68,25 +68,25 @@ func ExampleHorizontally() {
6868
table.Int("cuteness", 8, table.AlignRight, func(c Cat) int { return c.Cuteness }),
6969
)
7070

71-
board := ascii.Make(8, tbl.CumulativeFieldWidth)
71+
board := ascii.Make(8, 1)
7272
fmt.Println("Cool cats:")
7373
opts := table.RenderOptions{Orientation: table.Horizontally}
7474
tbl.Render(board.At(0, 0), opts, slices.Values([]Cat{
7575
{Name: "Chicken", Age: 5, Cuteness: 10},
7676
{Name: "Heart", Age: 4, Cuteness: 10},
7777
{Name: "Mai", Age: 2, Cuteness: 10},
78-
{Name: "Poi", Age: 15, Cuteness: 10},
78+
{Name: "Poi", Age: 150000000, Cuteness: 10},
7979
{Name: "Pigeon", Age: 2, Cuteness: 10},
80-
{Name: "Sugar", Age: 8, Cuteness: 10},
80+
{Name: "Sugar", Age: 8, Cuteness: 1000000000},
8181
{Name: "Yaya", Age: 5, Cuteness: 10},
8282
{Name: "Yuumi", Age: 5, Cuteness: 10},
83-
{Name: "Yuumibestcatever", Age: 5, Cuteness: 100000000},
83+
{Name: "Yuumibestcatever", Age: 5, Cuteness: 100},
8484
}))
8585
fmt.Println(board.String())
8686
// Output:
8787
// Cool cats:
88-
// name | Chicken Heart Mai Poi Pigeon Sugar Yaya Yuumi Yuumibestcatever
89-
// ---------+-------------------------------------------------------------------------
90-
// age | 5 4 2 15 2 8 5 5 5
91-
// cuteness | 10 10 10 10 10 10 10 10100000000
88+
// name | Chicken Heart Mai Poi Pigeon Sugar Yaya Yuumi Yuumibestcatever
89+
// ---------+----------------------------------------------------------------------------------
90+
// age | 5 4 2 150000000 2 8 5 5 5
91+
// cuteness | 10 10 10 10 10 1000000000 10 10 100
9292
}

internal/ascii/table/table.go

Lines changed: 70 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"math"
1111
"slices"
1212
"strconv"
13-
"strings"
1413

1514
"github.com/cockroachdb/crlib/crhumanize"
1615
"github.com/cockroachdb/pebble/internal/ascii"
@@ -21,7 +20,7 @@ import (
2120
//
2221
// Example:
2322
//
24-
// wb := ascii.Make(1, 10)
23+
// wb := ascii.Make(10, 10)
2524
// type Cat struct {
2625
// Name string
2726
// Age int
@@ -38,7 +37,7 @@ import (
3837
// Int("cuteness", 8, AlignRight, func(c Cat) int { return c.Cuteness }),
3938
// )
4039
//
41-
// wb.Reset(def.CumulativeFieldWidth)
40+
// wb.Reset(10)
4241
// def.Render(wb.At(0, 0), RenderOptions{}, cats)
4342
//
4443
// Output of wb.String():
@@ -48,32 +47,21 @@ import (
4847
// Mai 2 10
4948
// Yuumi 5 10
5049
func Define[T any](fields ...Element) Layout[T] {
51-
maxFieldWidth := 0
52-
for i := range len(fields) {
53-
maxFieldWidth = max(maxFieldWidth, fields[i].width())
54-
}
55-
56-
cumulativeFieldWidth := 0
57-
for i := range len(fields) {
58-
w := fields[i].width()
59-
h := fields[i].header(Vertically, maxFieldWidth)
60-
if len(h) > w {
61-
panic(fmt.Sprintf("header %q is too long for column %d", h, i))
50+
for i := range fields {
51+
if f, ok := fields[i].(Field[T]); ok {
52+
if h := f.header(); len(h) > f.width() {
53+
panic(fmt.Sprintf("header %q is too long for column %d", h, i))
54+
}
6255
}
63-
cumulativeFieldWidth += w
6456
}
6557
return Layout[T]{
66-
CumulativeFieldWidth: cumulativeFieldWidth,
67-
MaxFieldWidth: maxFieldWidth,
68-
fields: fields,
58+
fields: fields,
6959
}
7060
}
7161

7262
// A Layout defines the layout of a table.
7363
type Layout[T any] struct {
74-
CumulativeFieldWidth int
75-
MaxFieldWidth int
76-
fields []Element
64+
fields []Element
7765
}
7866

7967
// RenderOptions specifies the options for rendering a table.
@@ -85,9 +73,9 @@ type RenderOptions struct {
8573
// returning the modified cursor.
8674
func (d *Layout[T]) Render(start ascii.Cursor, opts RenderOptions, rows iter.Seq[T]) ascii.Cursor {
8775
cur := start
88-
tuples := slices.Collect(rows)
8976

9077
if opts.Orientation == Vertically {
78+
tuples := slices.Collect(rows)
9179
vals := make([]string, len(tuples))
9280
for fieldIdx, c := range d.fields {
9381
if fieldIdx > 0 {
@@ -105,8 +93,9 @@ func (d *Layout[T]) Render(start ascii.Cursor, opts RenderOptions, rows iter.Seq
10593
cur = cur.Offset(0, 1)
10694
continue
10795
}
96+
f := c.(Field[T])
10897
for i, t := range tuples {
109-
vals[i] = c.(Field[T]).renderValue(i, t)
98+
vals[i] = f.renderValue(i, t)
11099
}
111100

112101
width := c.width()
@@ -115,128 +104,95 @@ func (d *Layout[T]) Render(start ascii.Cursor, opts RenderOptions, rows iter.Seq
115104
for i := range vals {
116105
width = max(width, len(vals[i]))
117106
}
118-
header := c.header(Vertically, width)
119-
align := c.align()
120-
padding := width - len(header)
121-
cur.Offset(0, 0).WriteString(
122-
align.maybePadding(AlignRight, padding) + header + align.maybePadding(AlignLeft, padding),
123-
)
124-
cur.Offset(1, 0).WriteString(strings.Repeat("-", width))
125-
107+
header := f.header()
108+
align := f.align()
109+
pad(cur, width, align, header)
110+
cur.Down(1).RepeatByte(width, '-')
126111
for i := range vals {
127-
ctx := RenderContext[T]{
128-
Orientation: Vertically,
129-
Pos: cur.Offset(2+i, 0),
130-
MaxFieldWidth: d.MaxFieldWidth,
131-
}
132-
spec := widthStr(width, c.align()) + "s"
133-
ctx.PaddedPos(width).Printf(spec, vals[i])
112+
pad(cur.Down(2+i), width, align, vals[i])
134113
}
135-
cur = cur.Offset(0, width)
114+
cur = cur.Right(width)
136115
}
137-
return start.Offset(2+len(tuples), 0)
116+
return start.Down(2 + len(tuples))
117+
}
118+
119+
headerColumnWidth := 1
120+
for i := range d.fields {
121+
headerColumnWidth = max(headerColumnWidth, d.fields[i].width())
138122
}
139123

140124
for i := range d.fields {
141-
cur.Offset(i, 0).WriteString(d.fields[i].header(Horizontally, d.MaxFieldWidth))
142125
if _, ok := d.fields[i].(divider); ok {
143-
cur.Offset(i, d.MaxFieldWidth).WriteString("-+-")
126+
cur.Down(i).RepeatByte(headerColumnWidth, '-')
144127
} else {
145-
cur.Offset(i, d.MaxFieldWidth).WriteString(" | ")
128+
pad(cur.Down(i), headerColumnWidth, AlignRight, d.fields[i].(Field[T]).header())
146129
}
147130
}
131+
cur = cur.Right(headerColumnWidth)
132+
for i := range d.fields {
133+
if _, ok := d.fields[i].(divider); ok {
134+
cur.Down(i).WriteString("-+-")
135+
} else {
136+
cur.Down(i).WriteString(" | ")
137+
}
138+
}
139+
cur = cur.Right(3)
140+
148141
tupleIndex := 0
149-
c := d.MaxFieldWidth + 3
142+
colSpacing := 0
150143
for t := range rows {
144+
width := 1
145+
for i := range d.fields {
146+
if f, ok := d.fields[i].(Field[T]); ok {
147+
width = max(width, len(f.renderValue(tupleIndex, t)))
148+
}
149+
}
151150
for i := range d.fields {
152-
if div, ok := d.fields[i].(divider); ok {
153-
div.renderStatic(Horizontally, d.MaxFieldWidth, cur.Offset(i, c))
151+
if _, ok := d.fields[i].(divider); ok {
152+
cur.Down(i).RepeatByte(width+colSpacing, '-')
154153
} else {
155-
ctx := RenderContext[T]{
156-
Orientation: Horizontally,
157-
Pos: cur.Offset(i, c),
158-
MaxFieldWidth: d.MaxFieldWidth,
159-
}
160154
f := d.fields[i].(Field[T])
161-
width := f.width()
162-
spec := widthStr(width, f.align()) + "s"
163-
ctx.PaddedPos(width).Printf(spec, f.renderValue(tupleIndex, t))
155+
pad(cur.Down(i).Right(colSpacing), width, d.fields[i].align(), f.renderValue(tupleIndex, t))
164156
}
165157
}
166158
tupleIndex++
167-
c += d.MaxFieldWidth
159+
cur = cur.Right(width + colSpacing)
160+
colSpacing = 2
168161
}
169-
return cur.Offset(len(d.fields), c)
170-
}
171-
172-
// A RenderContext provides the context for rendering a table.
173-
type RenderContext[T any] struct {
174-
Orientation Orientation
175-
Pos ascii.Cursor
176-
MaxFieldWidth int
177-
}
178-
179-
func (c *RenderContext[T]) PaddedPos(width int) ascii.Cursor {
180-
if c.Orientation == Vertically {
181-
return c.Pos
182-
}
183-
// Horizontally, we need to pad the width to the max field width.
184-
return c.Pos.Offset(0, c.MaxFieldWidth-width)
162+
return start.Down(len(d.fields))
185163
}
186164

187165
// Element is the base interface, common to all table elements.
188166
type Element interface {
189-
header(o Orientation, maxWidth int) string
190167
width() int
191168
align() Align
192169
}
193170

194-
// StaticElement is an Element that doesn't depend on the tuple value for
195-
// rendering.
196-
type StaticElement interface {
197-
Element
198-
renderStatic(o Orientation, maxWidth int, pos ascii.Cursor)
199-
}
200-
201171
// Field is an Element that depends on the tuple value for rendering.
202172
type Field[T any] interface {
203173
Element
174+
header() string
204175
renderValue(tupleIndex int, tuple T) string
205176
}
206177

207178
// Div creates a divider field used to visually separate regions of the table.
208-
func Div() StaticElement {
179+
func Div() Element {
209180
return divider{}
210181
}
211182

212183
type divider struct{}
213184

214185
var (
215-
_ StaticElement = (*divider)(nil)
186+
_ Element = (*divider)(nil)
216187

217188
// TODO(jackson): The staticcheck tool doesn't recognize that these are used to
218189
// satisfy the Field interface. Why not?
219-
_ = divider.header
220190
_ = divider.width
221191
_ = divider.align
222-
_ = divider.renderStatic
223192
)
224193

225-
func (d divider) header(o Orientation, maxWidth int) string {
226-
if o == Horizontally {
227-
return strings.Repeat("-", maxWidth)
228-
}
229-
return " | "
230-
}
231-
func (d divider) width() int { return 3 }
194+
func (d divider) width() int { return 1 }
232195
func (d divider) align() Align { return AlignLeft }
233-
func (d divider) renderStatic(o Orientation, maxWidth int, pos ascii.Cursor) {
234-
if o == Horizontally {
235-
pos.RepeatByte(maxWidth, '-')
236-
} else {
237-
pos.WriteString(" | ")
238-
}
239-
}
240196

241197
func Literal[T any](s string) Field[T] {
242198
return literal[T](s)
@@ -255,9 +211,9 @@ var (
255211
_ = literal[any].renderValue
256212
)
257213

258-
func (l literal[T]) header(o Orientation, maxWidth int) string { return " " }
259-
func (l literal[T]) width() int { return len(l) }
260-
func (l literal[T]) align() Align { return AlignLeft }
214+
func (l literal[T]) header() string { return " " }
215+
func (l literal[T]) width() int { return len(l) }
216+
func (l literal[T]) align() Align { return AlignLeft }
261217
func (l literal[T]) renderValue(tupleIndex int, tuple T) string {
262218
return string(l)
263219
}
@@ -269,11 +225,18 @@ const (
269225

270226
type Align uint8
271227

272-
func (a Align) maybePadding(ifAlign Align, width int) string {
273-
if a == ifAlign {
274-
return strings.Repeat(" ", width)
228+
// pad writes the given string to the cursor, padding it to the given width
229+
// (according to the alignment).
230+
func pad(cur ascii.Cursor, toWidth int, align Align, s string) ascii.Cursor {
231+
if len(s) >= toWidth {
232+
return cur.WriteString(s)
275233
}
276-
return ""
234+
startCur := cur
235+
if align == AlignRight {
236+
cur = cur.Right(toWidth - len(s))
237+
}
238+
cur.WriteString(s)
239+
return startCur.Right(toWidth)
277240
}
278241

279242
const (
@@ -354,20 +317,13 @@ var (
354317
_ = (&funcField[any]{}).renderValue
355318
)
356319

357-
func (c *funcField[T]) header(o Orientation, maxWidth int) string { return c.headerValue }
358-
func (c *funcField[T]) width() int { return c.widthValue }
359-
func (c *funcField[T]) align() Align { return c.alignValue }
320+
func (c *funcField[T]) header() string { return c.headerValue }
321+
func (c *funcField[T]) width() int { return c.widthValue }
322+
func (c *funcField[T]) align() Align { return c.alignValue }
360323
func (c *funcField[T]) renderValue(tupleIndex int, tuple T) string {
361324
return c.toStringFn(tupleIndex, tuple)
362325
}
363326

364-
func widthStr(width int, align Align) string {
365-
if align == AlignLeft {
366-
return "%-" + strconv.Itoa(width)
367-
}
368-
return "%" + strconv.Itoa(width)
369-
}
370-
371327
// humanizeFloat formats a float64 value as a string. It shows up to two
372328
// decimals, depending on the target length. NaN is shown as "-".
373329
func humanizeFloat(v float64, targetLength int) string {

0 commit comments

Comments
 (0)