Skip to content

Commit

Permalink
table: ImportGrid to import a 1d or 2d grid as rows (#343)
Browse files Browse the repository at this point in the history
  • Loading branch information
jedib0t authored Nov 27, 2024
1 parent ada7500 commit 5994546
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 2 deletions.
4 changes: 2 additions & 2 deletions table/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
19 changes: 19 additions & 0 deletions table/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
123 changes: 123 additions & 0 deletions table/table_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package table

import (
"fmt"
"github.com/stretchr/testify/require"
"strings"
"testing"
"unicode/utf8"
Expand Down Expand Up @@ -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())
Expand Down
43 changes: 43 additions & 0 deletions table/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
21 changes: 21 additions & 0 deletions table/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
1 change: 1 addition & 0 deletions table/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 5994546

Please sign in to comment.