Skip to content

Commit

Permalink
Programmatic Sorting (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
Evertras committed Feb 23, 2022
1 parent 9a7bdba commit 517a14c
Show file tree
Hide file tree
Showing 12 changed files with 562 additions and 5 deletions.
4 changes: 4 additions & 0 deletions Makefile
Expand Up @@ -18,6 +18,10 @@ example-pagination:
example-simplest:
@go run ./examples/simplest/*.go

.PHONY: example-simplest
example-sorting:
@go run ./examples/sorting/*.go

.PHONY: test
test:
@go test -race -cover ./table
Expand Down
9 changes: 9 additions & 0 deletions README.md
Expand Up @@ -32,6 +32,15 @@ Can make rows selectable, and fetch the current selections.
Pagination can be set with a given page size, which automatically generates a
simple footer to show the current page and total pages.

Columns can be sorted in either ascending or descending order. Multiple columns
can be specified in a row. If multiple columns are specified, first the table
is sorted by the first specified column, then each group within that column is
sorted in smaller and smaller groups. [See the sorting example](examples/sorting/main.go)
for more information.

If a feature is confusing to use or could use a better example, please feel free
to open an issue.

## Defining table data

A table is defined by a list of `Column` values that define the columns in the
Expand Down
13 changes: 9 additions & 4 deletions examples/features/main.go
Expand Up @@ -47,7 +47,11 @@ type Model struct {

func NewModel() Model {
columns := []table.Column{
table.NewColumn(columnKeyID, "ID", 5).WithStyle(lipgloss.NewStyle().Faint(true).Foreground(lipgloss.Color("#88f"))),
table.NewColumn(columnKeyID, "ID", 5).WithStyle(
lipgloss.NewStyle().
Faint(true).
Foreground(lipgloss.Color("#88f")).
Align(lipgloss.Left)),
table.NewColumn(columnKeyName, "Name", 10),
table.NewColumn(columnKeyDescription, "Description", 30),
table.NewColumn(columnKeyCount, "#", 5),
Expand All @@ -74,13 +78,13 @@ func NewModel() Model {
columnKeyCount: table.NewStyledCell(0, lipgloss.NewStyle().Faint(true)),
}),
table.NewRow(table.RowData{
columnKeyID: "2pg",
columnKeyID: "spg",
columnKeyName: "Page 2",
columnKeyDescription: "Second page",
columnKeyCount: 2,
}),
table.NewRow(table.RowData{
columnKeyID: "2pg2",
columnKeyID: "spg2",
columnKeyName: "Page 2.1",
columnKeyDescription: "Second page again",
columnKeyCount: 4,
Expand All @@ -103,7 +107,8 @@ func NewModel() Model {
WithKeyMap(keys).
WithStaticFooter("Footer!").
WithPageSize(3).
WithSelectedText(" ", "✓"),
WithSelectedText(" ", "✓").
SortByAsc(columnKeyID),
}

model.updateFooter()
Expand Down
108 changes: 108 additions & 0 deletions examples/sorting/main.go
@@ -0,0 +1,108 @@
package main

import (
"log"
"strings"

tea "github.com/charmbracelet/bubbletea"
"github.com/evertras/bubble-table/table"
)

const (
columnKeyName = "name"
columnKeyType = "type"
columnKeyWins = "element"
)

type Model struct {
simpleTable table.Model

columnSortKey string
sortDirection string
}

func NewModel() Model {
return Model{
simpleTable: table.New([]table.Column{
table.NewColumn(columnKeyName, "Name", 13),
table.NewColumn(columnKeyType, "Type", 13),
table.NewColumn(columnKeyWins, "Wins", 5),
}).WithRows([]table.Row{
table.NewRow(table.RowData{
columnKeyName: "ピカピカ",
columnKeyType: "Pikachu",
columnKeyWins: 4,
}),
table.NewRow(table.RowData{
columnKeyName: "Alphonse",
columnKeyType: "Pikachu",
columnKeyWins: 13,
}),
table.NewRow(table.RowData{
columnKeyName: "Burninator",
columnKeyType: "Charmander",
columnKeyWins: 8,
}),
table.NewRow(table.RowData{
columnKeyName: "Dihydrogen Monoxide",
columnKeyType: "Squirtle",
columnKeyWins: 31,
}),
}),
}
}

func (m Model) Init() tea.Cmd {
return nil
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)

m.simpleTable, cmd = m.simpleTable.Update(msg)
cmds = append(cmds, cmd)

switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
cmds = append(cmds, tea.Quit)

case "n":
m.columnSortKey = columnKeyName
m.simpleTable = m.simpleTable.SortByAsc(m.columnSortKey)

case "t":
m.columnSortKey = columnKeyType
// Within the same type, order each by wins
m.simpleTable = m.simpleTable.SortByAsc(m.columnSortKey).ThenSortByDesc(columnKeyWins)

case "w":
m.columnSortKey = columnKeyWins
m.simpleTable = m.simpleTable.SortByDesc(m.columnSortKey)
}
}

return m, tea.Batch(cmds...)
}

func (m Model) View() string {
body := strings.Builder{}

body.WriteString("A sorted simple default table\nSort by (n)ame, (t)ype, or (w)ins\nCurrently sorting by: " + m.columnSortKey + "\nPress q or ctrl+c to quit\n\n")

body.WriteString(m.simpleTable.View())

return body.String()
}

func main() {
p := tea.NewProgram(NewModel())

if err := p.Start(); err != nil {
log.Fatal(err)
}
}
64 changes: 64 additions & 0 deletions table/data.go
@@ -0,0 +1,64 @@
package table

import "time"

// This is just a bunch of data type checks, so... no linting here
// nolint: cyclop
func asInt(data interface{}) (int64, bool) {
switch val := data.(type) {
case int:
return int64(val), true

case int8:
return int64(val), true

case int16:
return int64(val), true

case int32:
return int64(val), true

case int64:
return val, true

case uint:
return int64(val), true

case uint8:
return int64(val), true

case uint16:
return int64(val), true

case uint32:
return int64(val), true

case uint64:
return int64(val), true

case time.Duration:
return int64(val), true

case StyledCell:
return asInt(val.Data)
}

return 0, false
}

func asNumber(data interface{}) (float64, bool) {
switch val := data.(type) {
case float32:
return float64(val), true

case float64:
return val, true

case StyledCell:
return asNumber(val.Data)
}

intVal, isInt := asInt(data)

return float64(intVal), isInt
}
43 changes: 43 additions & 0 deletions table/data_test.go
@@ -0,0 +1,43 @@
package table

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestAsInt(t *testing.T) {
check := func(data interface{}, isInt bool, expectedValue int64) {
val, ok := asInt(data)
assert.Equal(t, isInt, ok)
assert.Equal(t, expectedValue, val)
}

check(3, true, 3)
check(3.3, false, 0)
check(int8(3), true, 3)
check(int16(3), true, 3)
check(int32(3), true, 3)
check(int64(3), true, 3)
check(uint(3), true, 3)
check(uint8(3), true, 3)
check(uint16(3), true, 3)
check(uint32(3), true, 3)
check(uint64(3), true, 3)
check(StyledCell{Data: 3}, true, 3)
check(time.Duration(3), true, 3)
}

func TestAsNumber(t *testing.T) {
check := func(data interface{}, isFloat bool, expectedValue float64) {
val, ok := asNumber(data)
assert.Equal(t, isFloat, ok)
assert.InDelta(t, expectedValue, val, 0.001)
}

check(uint32(3), true, 3)
check(3.3, true, 3.3)
check(float32(3.3), true, 3.3)
check(StyledCell{Data: 3.3}, true, 3.3)
}
4 changes: 4 additions & 0 deletions table/model.go
Expand Up @@ -40,6 +40,10 @@ type Model struct {
pageSize int
currentPage int

// Sorting
sortOrder []sortColumn
sortedRows []Row

// Internal cached calculations for reference
totalWidth int
}
Expand Down
1 change: 1 addition & 0 deletions table/options.go
Expand Up @@ -14,6 +14,7 @@ func (m Model) HeaderStyle(style lipgloss.Style) Model {
// WithRows sets the rows to show as data in the table.
func (m Model) WithRows(rows []Row) Model {
m.rows = rows
m.updateSortedRows()

return m
}
Expand Down
2 changes: 1 addition & 1 deletion table/row.go
Expand Up @@ -44,7 +44,7 @@ func (r Row) WithStyle(style lipgloss.Style) Row {
// nolint: cyclop
func (m Model) renderRow(rowIndex int, last bool) string {
numColumns := len(m.columns)
row := m.rows[rowIndex]
row := m.sortedRows[rowIndex]
highlighted := rowIndex == m.rowCursorIndex

columnStrings := []string{}
Expand Down

0 comments on commit 517a14c

Please sign in to comment.