From 5994546f18481af580601c8e570338bde0431d8d Mon Sep 17 00:00:00 2001 From: Naveen Mahalingam Date: Wed, 27 Nov 2024 12:13:34 -0800 Subject: [PATCH] table: ImportGrid to import a 1d or 2d grid as rows (#343) --- table/render_test.go | 4 +- table/table.go | 19 +++++++ table/table_test.go | 123 +++++++++++++++++++++++++++++++++++++++++++ table/util.go | 43 +++++++++++++++ table/util_test.go | 21 ++++++++ table/writer.go | 1 + 6 files changed, 209 insertions(+), 2 deletions(-) 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.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/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.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/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, "[ 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)) +} 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