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