Repository: jedib0t/go-pretty
Pull request #343 (2024-11-27 20:04:51)

table: ImportGrid to import a 1d or 2d grid as rows
## Proposed Changes
  - Provide an easy way to import a 1d or 2d array as rows into the Table



sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=jedib0t_go-pretty&pullRequest=343) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [6 New issues](https://sonarcloud.io/project/issues?id=jedib0t_go-pretty&pullRequest=343&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=jedib0t_go-pretty&pullRequest=343&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=jedib0t_go-pretty&pullRequest=343&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=jedib0t_go-pretty&pullRequest=343&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=jedib0t_go-pretty&pullRequest=343&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=jedib0t_go-pretty&pullRequest=343)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=jedib0t_go-pretty&pullRequest=343) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [6 New issues](https://sonarcloud.io/project/issues?id=jedib0t_go-pretty&pullRequest=343&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=jedib0t_go-pretty&pullRequest=343&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=jedib0t_go-pretty&pullRequest=343&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=jedib0t_go-pretty&pullRequest=343&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=jedib0t_go-pretty&pullRequest=343&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=jedib0t_go-pretty&pullRequest=343)


coveralls: ## Pull Request Test Coverage Report for [Build 12057501795](https://coveralls.io/builds/71090246)


### Details

* **45** of **45**   **(100.0%)**  changed or added relevant lines in **2** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **100.0%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/71090246/badge)](https://coveralls.io/builds/71090246) |
| :-- | --: |
| Change from base [Build 11880348638](https://coveralls.io/builds/70906997): |  0.0% |
| Covered Lines: | 3763 |
| Relevant Lines: | 3763 |

---
##### 💛  - [Coveralls](https://coveralls.io)

sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=jedib0t_go-pretty&pullRequest=343) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [6 New issues](https://sonarcloud.io/project/issues?id=jedib0t_go-pretty&pullRequest=343&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=jedib0t_go-pretty&pullRequest=343&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=jedib0t_go-pretty&pullRequest=343&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=jedib0t_go-pretty&pullRequest=343&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=jedib0t_go-pretty&pullRequest=343&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=jedib0t_go-pretty&pullRequest=343)



Patch:
diff --git a/table/table.go b/table/table.go
index 35426c5..6cf0433 100644
--- a/table/table.go
+++ b/table/table.go
@@ -186,6 +186,25 @@ func (t *Table) AppendSeparator() {
 	}
 }
 
+// ImportGrid helps import 1d or 2d arrays as rows.
+func (t *Table) ImportGrid(grid interface{}) bool {
+	rows := objAsSlice(grid)
+	if rows == nil {
+		return false
+	}
+	addedRows := false
+	for _, row := range rows {
+		rowAsSlice := objAsSlice(row)
+		if rowAsSlice != nil {
+			t.AppendRow(rowAsSlice)
+		} else if row != nil {
+			t.AppendRow(Row{row})
+		}
+		addedRows = true
+	}
+	return addedRows
+}
+
 // Length returns the number of rows to be rendered.
 func (t *Table) Length() int {
 	return len(t.rowsRaw)
diff --git a/table/util.go b/table/util.go
index aa71012..6b7a658 100644
--- a/table/util.go
+++ b/table/util.go
@@ -67,3 +67,46 @@ func (m mergedColumnIndices) safeAppend(colIdx, otherColIdx int) {
 	}
 	m[otherColIdx][colIdx] = true
 }
+
+func objAsSlice(in interface{}) []interface{} {
+	var out []interface{}
+	if in != nil {
+		// dereference pointers
+		val := reflect.ValueOf(in)
+		if val.Kind() == reflect.Ptr && !val.IsNil() {
+			in = val.Elem().Interface()
+		}
+
+		if objIsSlice(in) {
+			v := reflect.ValueOf(in)
+			for i := 0; i < v.Len(); i++ {
+				// dereference pointers
+				v2 := v.Index(i)
+				if v2.Kind() == reflect.Ptr && !v2.IsNil() {
+					v2 = reflect.ValueOf(v2.Elem().Interface())
+				}
+
+				out = append(out, v2.Interface())
+			}
+		}
+	}
+
+	// remove trailing nil pointers
+	tailIdx := len(out)
+	for i := len(out) - 1; i >= 0; i-- {
+		val := reflect.ValueOf(out[i])
+		if val.Kind() != reflect.Ptr || !val.IsNil() {
+			break
+		}
+		tailIdx = i
+	}
+	return out[:tailIdx]
+}
+
+func objIsSlice(in interface{}) bool {
+	if in == nil {
+		return false
+	}
+	k := reflect.TypeOf(in).Kind()
+	return k == reflect.Slice || k == reflect.Array
+}
diff --git a/table/writer.go b/table/writer.go
index ec7a53a..406aaab 100644
--- a/table/writer.go
+++ b/table/writer.go
@@ -11,6 +11,7 @@ type Writer interface {
 	AppendRow(row Row, configs ...RowConfig)
 	AppendRows(rows []Row, configs ...RowConfig)
 	AppendSeparator()
+	ImportGrid(grid interface{}) bool
 	Length() int
 	Pager(opts ...PagerOption) Pager
 	Render() string


Test patch:
diff --git a/table/render_test.go b/table/render_test.go
index 4451cc3..b0ad3ed 100644
--- a/table/render_test.go
+++ b/table/render_test.go
@@ -10,11 +10,11 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func compareOutput(t *testing.T, out string, expectedOut string) {
+func compareOutput(t *testing.T, out string, expectedOut string, message ...interface{}) {
 	if strings.HasPrefix(expectedOut, "\n") {
 		expectedOut = strings.Replace(expectedOut, "\n", "", 1)
 	}
-	assert.Equal(t, expectedOut, out)
+	assert.Equal(t, expectedOut, out, message...)
 	if out != expectedOut {
 		fmt.Printf("Expected:\n%s\nActual:\n%s\n", expectedOut, out)
 	} else {
diff --git a/table/table_test.go b/table/table_test.go
index 4eb7951..7ac01b0 100644
--- a/table/table_test.go
+++ b/table/table_test.go
@@ -1,6 +1,8 @@
 package table
 
 import (
+	"fmt"
+	"github.com/stretchr/testify/require"
 	"strings"
 	"testing"
 	"unicode/utf8"
@@ -150,6 +152,127 @@ func TestTable_AppendRows(t *testing.T) {
 	assert.True(t, table.rowsConfigMap[3].AutoMerge)
 }
 
+func TestTable_ImportGrid(t *testing.T) {
+	t.Run("invalid grid", func(t *testing.T) {
+		table := Table{}
+
+		assert.False(t, table.ImportGrid(nil))
+		require.Len(t, table.rowsRaw, 0)
+
+		assert.False(t, table.ImportGrid(123))
+		require.Len(t, table.rowsRaw, 0)
+
+		assert.False(t, table.ImportGrid("abc"))
+		require.Len(t, table.rowsRaw, 0)
+
+		assert.False(t, table.ImportGrid(Table{}))
+		require.Len(t, table.rowsRaw, 0)
+
+		assert.False(t, table.ImportGrid(&Table{}))
+		require.Len(t, table.rowsRaw, 0)
+	})
+
+	a, b, c := 1, 2, 3
+	d, e, f := 4, 5, 6
+	g, h, i := 7, 8, 9
+
+	t.Run("valid 1d", func(t *testing.T) {
+		inputs := []interface{}{
+			[3]int{a, b, c},     // array
+			[]int{a, b, c},      // slice
+			&[]int{a, b, c},     // pointer to slice
+			[]*int{&a, &b, &c},  // slice of pointers-to-slices
+			&[]*int{&a, &b, &c}, // pointer to slice of pointers
+		}
+
+		for _, grid := range inputs {
+			message := fmt.Sprintf("grid: %#v", grid)
+
+			table := Table{}
+			table.Style().Options.SeparateRows = true
+			assert.True(t, table.ImportGrid(grid), message)
+			compareOutput(t, table.Render(), `
++---+
+| 1 |
++---+
+| 2 |
++---+
+| 3 |
++---+`, message)
+		}
+	})
+
+	t.Run("valid 2d", func(t *testing.T) {
+		inputs := []interface{}{
+			[3][3]int{{a, b, c}, {d, e, f}, {g, h, i}},           // array of arrays
+			[3][]int{{a, b, c}, {d, e, f}, {g, h, i}},            // array of slices
+			[][]int{{a, b, c}, {d, e, f}, {g, h, i}},             // slice of slices
+			&[][]int{{a, b, c}, {d, e, f}, {g, h, i}},            // pointer-to-slice of slices
+			[]*[]int{{a, b, c}, {d, e, f}, {g, h, i}},            // slice of pointers-to-slices
+			&[]*[]int{{a, b, c}, {d, e, f}, {g, h, i}},           // pointer-to-slice of pointers-to-slices
+			&[]*[]*int{{&a, &b, &c}, {&d, &e, &f}, {&g, &h, &i}}, // pointer-to-slice of pointers-to-slices of pointers
+		}
+
+		for _, grid := range inputs {
+			message := fmt.Sprintf("grid: %#v", grid)
+
+			table := Table{}
+			table.Style().Options.SeparateRows = true
+			assert.True(t, table.ImportGrid(grid), message)
+			compareOutput(t, table.Render(), `
++---+---+---+
+| 1 | 2 | 3 |
++---+---+---+
+| 4 | 5 | 6 |
++---+---+---+
+| 7 | 8 | 9 |
++---+---+---+`, message)
+		}
+	})
+
+	t.Run("valid 2d with nil rows", func(t *testing.T) {
+		inputs := []interface{}{
+			[]*[]int{{a, b, c}, {d, e, f}, nil},         // slice of pointers-to-slices
+			&[]*[]int{{a, b, c}, {d, e, f}, nil},        // pointer-to-slice of pointers-to-slices
+			&[]*[]*int{{&a, &b, &c}, {&d, &e, &f}, nil}, // pointer-to-slice of pointers-to-slices of pointers
+		}
+
+		for _, grid := range inputs {
+			message := fmt.Sprintf("grid: %#v", grid)
+
+			table := Table{}
+			table.Style().Options.SeparateRows = true
+			assert.True(t, table.ImportGrid(grid), message)
+			compareOutput(t, table.Render(), `
++---+---+---+
+| 1 | 2 | 3 |
++---+---+---+
+| 4 | 5 | 6 |
++---+---+---+`, message)
+		}
+	})
+
+	t.Run("valid 2d with nil columns and rows", func(t *testing.T) {
+		inputs := []interface{}{
+			&[]*[]*int{{&a, &b, &c}, {&d, &e, nil}, nil},
+		}
+
+		for _, grid := range inputs {
+			message := fmt.Sprintf("grid: %#v", grid)
+
+			table := Table{}
+			table.Style().Options.SeparateRows = true
+			assert.True(t, table.ImportGrid(grid), message)
+			compareOutput(t, table.Render(), `
++---+---+---+
+| 1 | 2 | 3 |
++---+---+---+
+| 4 | 5 |   |
++---+---+---+`, message)
+		}
+	})
+}
+
 func TestTable_Length(t *testing.T) {
 	table := Table{}
 	assert.Zero(t, table.Length())
diff --git a/table/util_test.go b/table/util_test.go
index b28ca7c..d176bca 100644
--- a/table/util_test.go
+++ b/table/util_test.go
@@ -51,3 +51,24 @@ func TestIsNumber(t *testing.T) {
 	assert.False(t, isNumber("1"))
 	assert.False(t, isNumber(nil))
 }
+
+func Test_objAsSlice(t *testing.T) {
+	a, b, c := 1, 2, 3
+	assert.Equal(t, "[1 2 3]", fmt.Sprint(objAsSlice([]int{a, b, c})))
+	assert.Equal(t, "[1 2 3]", fmt.Sprint(objAsSlice(&[]int{a, b, c})))
+	assert.Equal(t, "[1 2 3]", fmt.Sprint(objAsSlice(&[]*int{&a, &b, &c})))
+	assert.Equal(t, "[1 2]", fmt.Sprint(objAsSlice(&[]*int{&a, &b, nil})))
+	assert.Equal(t, "[1]", fmt.Sprint(objAsSlice(&[]*int{&a, nil, nil})))
+	assert.Equal(t, "[]", fmt.Sprint(objAsSlice(&[]*int{nil, nil, nil})))
+	assert.Equal(t, "[<nil> 2]", fmt.Sprint(objAsSlice(&[]*int{nil, &b, nil})))
+}
+
+func Test_objIsSlice(t *testing.T) {
+	assert.True(t, objIsSlice([]int{}))
+	assert.True(t, objIsSlice([]*int{}))
+	assert.False(t, objIsSlice(&[]int{}))
+	assert.False(t, objIsSlice(&[]*int{}))
+	assert.False(t, objIsSlice(Table{}))
+	assert.False(t, objIsSlice(&Table{}))
+	assert.False(t, objIsSlice(nil))
+}