Skip to content

Commit

Permalink
table: RenderTSV to render in tab-separated-values format (#277)
Browse files Browse the repository at this point in the history
  • Loading branch information
rafiramadhana committed Oct 4, 2023
1 parent 05c0986 commit 17050b7
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 6 deletions.
73 changes: 73 additions & 0 deletions table/render_tsv.go
@@ -0,0 +1,73 @@
package table

import (
"fmt"
"strings"
)

func (t *Table) RenderTSV() string {
t.initForRender()

var out strings.Builder

if t.numColumns > 0 {
if t.title != "" {
out.WriteString(t.title)
}

if t.autoIndex && len(t.rowsHeader) == 0 {
t.tsvRenderRow(&out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true})
}

t.tsvRenderRows(&out, t.rowsHeader, renderHint{isHeaderRow: true})
t.tsvRenderRows(&out, t.rows, renderHint{})
t.tsvRenderRows(&out, t.rowsFooter, renderHint{isFooterRow: true})

if t.caption != "" {
out.WriteRune('\n')
out.WriteString(t.caption)
}
}

return t.render(&out)
}

func (t *Table) tsvRenderRow(out *strings.Builder, row rowStr, hint renderHint) {
if out.Len() > 0 {
out.WriteRune('\n')
}

for idx, col := range row {
if idx == 0 && t.autoIndex {
if hint.isRegularRow() {
out.WriteString(fmt.Sprint(hint.rowNumber))
}
out.WriteRune('\t')
}

if idx > 0 {
out.WriteRune('\t')
}

if strings.ContainsAny(col, "\t\n\"") || strings.Contains(col, " ") {
out.WriteString(fmt.Sprintf("\"%s\"", t.tsvFixDoubleQuotes(col)))
} else {
out.WriteString(col)
}
}

for colIdx := len(row); colIdx < t.numColumns; colIdx++ {
out.WriteRune('\t')
}
}

func (t *Table) tsvFixDoubleQuotes(str string) string {
return strings.Replace(str, "\"", "\"\"", -1)
}

func (t *Table) tsvRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
for idx, row := range rows {
hint.rowNumber = idx + 1
t.tsvRenderRow(out, row, hint)
}
}
187 changes: 187 additions & 0 deletions table/render_tsv_test.go
@@ -0,0 +1,187 @@
package table

import (
"fmt"
"testing"
)

func TestTable_RenderTSV(t *testing.T) {
tests := []struct {
name string
tw func() Writer
output string
}{
{
tw: func() Writer {
tw := NewWriter()
tw.AppendHeader(testHeader)
tw.AppendRows(testRows)
tw.AppendRow(testRowMultiLine)
tw.AppendRow(testRowTabs)
tw.AppendRow(testRowDoubleQuotes)
tw.AppendFooter(testFooter)
tw.SetCaption(testCaption)
tw.SetTitle(testTitle1)
return tw
},
output: `
Game of Thrones
# First Name Last Name Salary
1 Arya Stark 3000
20 Jon Snow 2000 You know nothing, Jon Snow!
300 Tyrion Lannister 5000
0 Winter Is 0 "Coming.
The North Remembers!
This is known."
0 Valar Morghulis 0 "Faceless Men"
0 Valar Morghulis 0 "Faceless""Men"
Total 10000
A Song of Ice and Fire`,
},
{
name: "Auto index",
tw: func() Writer {
tw := NewWriter()
for rowIdx := 0; rowIdx < 10; rowIdx++ {
row := make(Row, 10)
for colIdx := 0; colIdx < 10; colIdx++ {
row[colIdx] = fmt.Sprintf("%s%d", AutoIndexColumnID(colIdx), rowIdx+1)
}
tw.AppendRow(row)
}
for rowIdx := 0; rowIdx < 1; rowIdx++ {
row := make(Row, 10)
for colIdx := 0; colIdx < 10; colIdx++ {
row[colIdx] = AutoIndexColumnID(colIdx) + "F"
}
tw.AppendFooter(row)
}
tw.SetAutoIndex(true)
tw.SetStyle(StyleLight)
return tw
},
output: `
A B C D E F G H I J
1 A1 B1 C1 D1 E1 F1 G1 H1 I1 J1
2 A2 B2 C2 D2 E2 F2 G2 H2 I2 J2
3 A3 B3 C3 D3 E3 F3 G3 H3 I3 J3
4 A4 B4 C4 D4 E4 F4 G4 H4 I4 J4
5 A5 B5 C5 D5 E5 F5 G5 H5 I5 J5
6 A6 B6 C6 D6 E6 F6 G6 H6 I6 J6
7 A7 B7 C7 D7 E7 F7 G7 H7 I7 J7
8 A8 B8 C8 D8 E8 F8 G8 H8 I8 J8
9 A9 B9 C9 D9 E9 F9 G9 H9 I9 J9
10 A10 B10 C10 D10 E10 F10 G10 H10 I10 J10
AF BF CF DF EF FF GF HF IF JF`,
},
{
name: "Empty",
tw: func() Writer {
tw := NewWriter()
return tw
},
output: ``,
},
{
name: "Every column hidden",
tw: func() Writer {
tw := NewWriter()
tw.AppendHeader(testHeader)
tw.AppendRows(testRows)
tw.AppendFooter(testFooter)
tw.SortBy([]SortBy{
{Name: "Salary", Mode: DscNumeric},
})
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0, 1, 2, 3, 4}))
return tw
},
output: ``,
},
{
name: "First column hidden",
tw: func() Writer {
tw := NewWriter()
tw.AppendHeader(testHeader)
tw.AppendRows(testRows)
tw.AppendFooter(testFooter)
tw.SortBy([]SortBy{
{Name: "Salary", Mode: DscNumeric},
})
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0}))
return tw
},
output: `
First Name Last Name Salary
>>Tyrion Lannister<< 5013
>>Arya Stark<< 3013
>>Jon Snow<< 2013 ~You know nothing, Jon Snow!~
Total 10000 `,
},
{
name: "Column hidden in the middle",
tw: func() Writer {
tw := NewWriter()
tw.AppendHeader(testHeader)
tw.AppendRows(testRows)
tw.AppendFooter(testFooter)
tw.SortBy([]SortBy{
{Name: "Salary", Mode: DscNumeric},
})
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{1}))
return tw
},
output: `
# Last Name Salary
307 Lannister<< 5013
8 Stark<< 3013
27 Snow<< 2013 ~You know nothing, Jon Snow!~
Total 10000 `,
},
{
name: "Last column hidden",
tw: func() Writer {
tw := NewWriter()
tw.AppendHeader(testHeader)
tw.AppendRows(testRows)
tw.AppendFooter(testFooter)
tw.SortBy([]SortBy{
{Name: "Salary", Mode: DscNumeric},
})
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{4}))
return tw
},
output: `
# First Name Last Name Salary
307 >>Tyrion Lannister<< 5013
8 >>Arya Stark<< 3013
27 >>Jon Snow<< 2013
Total 10000`,
},
{
name: "Sorted",
tw: func() Writer {
tw := NewWriter()
tw.AppendHeader(testHeader)
tw.AppendRows(testRows)
tw.AppendRow(Row{11, "Sansa", "Stark", 6000})
tw.AppendFooter(testFooter)
tw.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}})
return tw
},
output: `
# First Name Last Name Salary
300 Tyrion Lannister 5000
20 Jon Snow 2000 You know nothing, Jon Snow!
1 Arya Stark 3000
11 Sansa Stark 6000
Total 10000 `,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.tw().RenderTSV()
compareOutput(t, output, tt.output)
})
}
}
13 changes: 7 additions & 6 deletions table/table_test.go
Expand Up @@ -24,12 +24,13 @@ var (
{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
{300, "Tyrion", "Lannister", 5000},
}
testRowMultiLine = Row{0, "Winter", "Is", 0, "Coming.\r\nThe North Remembers!\nThis is known."}
testRowNewLines = Row{0, "Valar", "Morghulis", 0, "Faceless\nMen"}
testRowPipes = Row{0, "Valar", "Morghulis", 0, "Faceless|Men"}
testRowTabs = Row{0, "Valar", "Morghulis", 0, "Faceless\tMen"}
testTitle1 = "Game of Thrones"
testTitle2 = "When you play the Game of Thrones, you win or you die. There is no middle ground."
testRowMultiLine = Row{0, "Winter", "Is", 0, "Coming.\r\nThe North Remembers!\nThis is known."}
testRowNewLines = Row{0, "Valar", "Morghulis", 0, "Faceless\nMen"}
testRowPipes = Row{0, "Valar", "Morghulis", 0, "Faceless|Men"}
testRowTabs = Row{0, "Valar", "Morghulis", 0, "Faceless\tMen"}
testRowDoubleQuotes = Row{0, "Valar", "Morghulis", 0, "Faceless\"Men"}
testTitle1 = "Game of Thrones"
testTitle2 = "When you play the Game of Thrones, you win or you die. There is no middle ground."
)

func init() {
Expand Down
1 change: 1 addition & 0 deletions table/writer.go
Expand Up @@ -16,6 +16,7 @@ type Writer interface {
RenderCSV() string
RenderHTML() string
RenderMarkdown() string
RenderTSV() string
ResetFooters()
ResetHeaders()
ResetRows()
Expand Down

0 comments on commit 17050b7

Please sign in to comment.