Repository: jedib0t/go-pretty (go)

Issue:
Table - Allow `RenderCsv` to be formatted with tabs instead of comma
`RenderCsv` has the comma separator hardcoded.

I would like to render the CSV using tabs because it is easier to import the generated file in Google Sheets of Excel by just pasting the CSV content.

Pull request:
table: RenderTSV to render in tab-separated-values format
## Proposed Changes
  - Add `RenderTSV() string` to `Writer` interface
  - Add `RenderTSV()` implementation

Fixes #276 .

@jedib0t @dinhani

Patch:
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/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()


Test patch:
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() {