diff --git a/table/render_tsv.go b/table/render_tsv.go new file mode 100644 index 0000000..bb73975 --- /dev/null +++ b/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) + } +} diff --git a/table/render_tsv_test.go b/table/render_tsv_test.go new file mode 100644 index 0000000..c94b886 --- /dev/null +++ b/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) + }) + } +} diff --git a/table/table_test.go b/table/table_test.go index 0116ec6..d4f9955 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -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() { diff --git a/table/writer.go b/table/writer.go index c5ecb3e..c08195b 100644 --- a/table/writer.go +++ b/table/writer.go @@ -16,6 +16,7 @@ type Writer interface { RenderCSV() string RenderHTML() string RenderMarkdown() string + RenderTSV() string ResetFooters() ResetHeaders() ResetRows()