Skip to content

Commit 17050b7

Browse files
table: RenderTSV to render in tab-separated-values format (#277)
1 parent 05c0986 commit 17050b7

File tree

4 files changed

+268
-6
lines changed

4 files changed

+268
-6
lines changed

table/render_tsv.go

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package table
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
func (t *Table) RenderTSV() string {
9+
t.initForRender()
10+
11+
var out strings.Builder
12+
13+
if t.numColumns > 0 {
14+
if t.title != "" {
15+
out.WriteString(t.title)
16+
}
17+
18+
if t.autoIndex && len(t.rowsHeader) == 0 {
19+
t.tsvRenderRow(&out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true})
20+
}
21+
22+
t.tsvRenderRows(&out, t.rowsHeader, renderHint{isHeaderRow: true})
23+
t.tsvRenderRows(&out, t.rows, renderHint{})
24+
t.tsvRenderRows(&out, t.rowsFooter, renderHint{isFooterRow: true})
25+
26+
if t.caption != "" {
27+
out.WriteRune('\n')
28+
out.WriteString(t.caption)
29+
}
30+
}
31+
32+
return t.render(&out)
33+
}
34+
35+
func (t *Table) tsvRenderRow(out *strings.Builder, row rowStr, hint renderHint) {
36+
if out.Len() > 0 {
37+
out.WriteRune('\n')
38+
}
39+
40+
for idx, col := range row {
41+
if idx == 0 && t.autoIndex {
42+
if hint.isRegularRow() {
43+
out.WriteString(fmt.Sprint(hint.rowNumber))
44+
}
45+
out.WriteRune('\t')
46+
}
47+
48+
if idx > 0 {
49+
out.WriteRune('\t')
50+
}
51+
52+
if strings.ContainsAny(col, "\t\n\"") || strings.Contains(col, " ") {
53+
out.WriteString(fmt.Sprintf("\"%s\"", t.tsvFixDoubleQuotes(col)))
54+
} else {
55+
out.WriteString(col)
56+
}
57+
}
58+
59+
for colIdx := len(row); colIdx < t.numColumns; colIdx++ {
60+
out.WriteRune('\t')
61+
}
62+
}
63+
64+
func (t *Table) tsvFixDoubleQuotes(str string) string {
65+
return strings.Replace(str, "\"", "\"\"", -1)
66+
}
67+
68+
func (t *Table) tsvRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
69+
for idx, row := range rows {
70+
hint.rowNumber = idx + 1
71+
t.tsvRenderRow(out, row, hint)
72+
}
73+
}

table/render_tsv_test.go

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package table
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
)
7+
8+
func TestTable_RenderTSV(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
tw func() Writer
12+
output string
13+
}{
14+
{
15+
tw: func() Writer {
16+
tw := NewWriter()
17+
tw.AppendHeader(testHeader)
18+
tw.AppendRows(testRows)
19+
tw.AppendRow(testRowMultiLine)
20+
tw.AppendRow(testRowTabs)
21+
tw.AppendRow(testRowDoubleQuotes)
22+
tw.AppendFooter(testFooter)
23+
tw.SetCaption(testCaption)
24+
tw.SetTitle(testTitle1)
25+
return tw
26+
},
27+
output: `
28+
Game of Thrones
29+
# First Name Last Name Salary
30+
1 Arya Stark 3000
31+
20 Jon Snow 2000 You know nothing, Jon Snow!
32+
300 Tyrion Lannister 5000
33+
0 Winter Is 0 "Coming.
34+
The North Remembers!
35+
This is known."
36+
0 Valar Morghulis 0 "Faceless Men"
37+
0 Valar Morghulis 0 "Faceless""Men"
38+
Total 10000
39+
A Song of Ice and Fire`,
40+
},
41+
{
42+
name: "Auto index",
43+
tw: func() Writer {
44+
tw := NewWriter()
45+
for rowIdx := 0; rowIdx < 10; rowIdx++ {
46+
row := make(Row, 10)
47+
for colIdx := 0; colIdx < 10; colIdx++ {
48+
row[colIdx] = fmt.Sprintf("%s%d", AutoIndexColumnID(colIdx), rowIdx+1)
49+
}
50+
tw.AppendRow(row)
51+
}
52+
for rowIdx := 0; rowIdx < 1; rowIdx++ {
53+
row := make(Row, 10)
54+
for colIdx := 0; colIdx < 10; colIdx++ {
55+
row[colIdx] = AutoIndexColumnID(colIdx) + "F"
56+
}
57+
tw.AppendFooter(row)
58+
}
59+
tw.SetAutoIndex(true)
60+
tw.SetStyle(StyleLight)
61+
return tw
62+
},
63+
output: `
64+
A B C D E F G H I J
65+
1 A1 B1 C1 D1 E1 F1 G1 H1 I1 J1
66+
2 A2 B2 C2 D2 E2 F2 G2 H2 I2 J2
67+
3 A3 B3 C3 D3 E3 F3 G3 H3 I3 J3
68+
4 A4 B4 C4 D4 E4 F4 G4 H4 I4 J4
69+
5 A5 B5 C5 D5 E5 F5 G5 H5 I5 J5
70+
6 A6 B6 C6 D6 E6 F6 G6 H6 I6 J6
71+
7 A7 B7 C7 D7 E7 F7 G7 H7 I7 J7
72+
8 A8 B8 C8 D8 E8 F8 G8 H8 I8 J8
73+
9 A9 B9 C9 D9 E9 F9 G9 H9 I9 J9
74+
10 A10 B10 C10 D10 E10 F10 G10 H10 I10 J10
75+
AF BF CF DF EF FF GF HF IF JF`,
76+
},
77+
{
78+
name: "Empty",
79+
tw: func() Writer {
80+
tw := NewWriter()
81+
return tw
82+
},
83+
output: ``,
84+
},
85+
{
86+
name: "Every column hidden",
87+
tw: func() Writer {
88+
tw := NewWriter()
89+
tw.AppendHeader(testHeader)
90+
tw.AppendRows(testRows)
91+
tw.AppendFooter(testFooter)
92+
tw.SortBy([]SortBy{
93+
{Name: "Salary", Mode: DscNumeric},
94+
})
95+
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0, 1, 2, 3, 4}))
96+
return tw
97+
},
98+
output: ``,
99+
},
100+
{
101+
name: "First column hidden",
102+
tw: func() Writer {
103+
tw := NewWriter()
104+
tw.AppendHeader(testHeader)
105+
tw.AppendRows(testRows)
106+
tw.AppendFooter(testFooter)
107+
tw.SortBy([]SortBy{
108+
{Name: "Salary", Mode: DscNumeric},
109+
})
110+
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0}))
111+
return tw
112+
},
113+
output: `
114+
First Name Last Name Salary
115+
>>Tyrion Lannister<< 5013
116+
>>Arya Stark<< 3013
117+
>>Jon Snow<< 2013 ~You know nothing, Jon Snow!~
118+
Total 10000 `,
119+
},
120+
{
121+
name: "Column hidden in the middle",
122+
tw: func() Writer {
123+
tw := NewWriter()
124+
tw.AppendHeader(testHeader)
125+
tw.AppendRows(testRows)
126+
tw.AppendFooter(testFooter)
127+
tw.SortBy([]SortBy{
128+
{Name: "Salary", Mode: DscNumeric},
129+
})
130+
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{1}))
131+
return tw
132+
},
133+
output: `
134+
# Last Name Salary
135+
307 Lannister<< 5013
136+
8 Stark<< 3013
137+
27 Snow<< 2013 ~You know nothing, Jon Snow!~
138+
Total 10000 `,
139+
},
140+
{
141+
name: "Last column hidden",
142+
tw: func() Writer {
143+
tw := NewWriter()
144+
tw.AppendHeader(testHeader)
145+
tw.AppendRows(testRows)
146+
tw.AppendFooter(testFooter)
147+
tw.SortBy([]SortBy{
148+
{Name: "Salary", Mode: DscNumeric},
149+
})
150+
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{4}))
151+
return tw
152+
},
153+
output: `
154+
# First Name Last Name Salary
155+
307 >>Tyrion Lannister<< 5013
156+
8 >>Arya Stark<< 3013
157+
27 >>Jon Snow<< 2013
158+
Total 10000`,
159+
},
160+
{
161+
name: "Sorted",
162+
tw: func() Writer {
163+
tw := NewWriter()
164+
tw.AppendHeader(testHeader)
165+
tw.AppendRows(testRows)
166+
tw.AppendRow(Row{11, "Sansa", "Stark", 6000})
167+
tw.AppendFooter(testFooter)
168+
tw.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}})
169+
return tw
170+
},
171+
output: `
172+
# First Name Last Name Salary
173+
300 Tyrion Lannister 5000
174+
20 Jon Snow 2000 You know nothing, Jon Snow!
175+
1 Arya Stark 3000
176+
11 Sansa Stark 6000
177+
Total 10000 `,
178+
},
179+
}
180+
181+
for _, tt := range tests {
182+
t.Run(tt.name, func(t *testing.T) {
183+
output := tt.tw().RenderTSV()
184+
compareOutput(t, output, tt.output)
185+
})
186+
}
187+
}

table/table_test.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@ var (
2424
{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
2525
{300, "Tyrion", "Lannister", 5000},
2626
}
27-
testRowMultiLine = Row{0, "Winter", "Is", 0, "Coming.\r\nThe North Remembers!\nThis is known."}
28-
testRowNewLines = Row{0, "Valar", "Morghulis", 0, "Faceless\nMen"}
29-
testRowPipes = Row{0, "Valar", "Morghulis", 0, "Faceless|Men"}
30-
testRowTabs = Row{0, "Valar", "Morghulis", 0, "Faceless\tMen"}
31-
testTitle1 = "Game of Thrones"
32-
testTitle2 = "When you play the Game of Thrones, you win or you die. There is no middle ground."
27+
testRowMultiLine = Row{0, "Winter", "Is", 0, "Coming.\r\nThe North Remembers!\nThis is known."}
28+
testRowNewLines = Row{0, "Valar", "Morghulis", 0, "Faceless\nMen"}
29+
testRowPipes = Row{0, "Valar", "Morghulis", 0, "Faceless|Men"}
30+
testRowTabs = Row{0, "Valar", "Morghulis", 0, "Faceless\tMen"}
31+
testRowDoubleQuotes = Row{0, "Valar", "Morghulis", 0, "Faceless\"Men"}
32+
testTitle1 = "Game of Thrones"
33+
testTitle2 = "When you play the Game of Thrones, you win or you die. There is no middle ground."
3334
)
3435

3536
func init() {

table/writer.go

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type Writer interface {
1616
RenderCSV() string
1717
RenderHTML() string
1818
RenderMarkdown() string
19+
RenderTSV() string
1920
ResetFooters()
2021
ResetHeaders()
2122
ResetRows()

0 commit comments

Comments
 (0)