From 090b0af9867a29cc58bf4766244b118fd526d6d9 Mon Sep 17 00:00:00 2001 From: JediB0T Date: Mon, 21 May 2018 21:10:30 -0700 Subject: [PATCH] table: support sorting by any of the columns (#41) --- README.md | 1 + progress/tracker.go | 83 ----------------------- progress/tracker_sort.go | 85 +++++++++++++++++++++++ progress/tracker_sort_test.go | 50 ++++++++++++++ progress/tracker_test.go | 45 +------------ table/README.md | 1 + table/render.go | 9 ++- table/render_test.go | 23 +++++++ table/sort.go | 120 +++++++++++++++++++++++++++++++++ table/sort_test.go | 123 ++++++++++++++++++++++++++++++++++ table/table.go | 45 ++++++++----- table/table_test.go | 11 +++ table/writer.go | 1 + util/auto_index.go | 2 +- 14 files changed, 454 insertions(+), 145 deletions(-) create mode 100644 progress/tracker_sort.go create mode 100644 progress/tracker_sort_test.go create mode 100644 table/sort.go create mode 100644 table/sort_test.go diff --git a/README.md b/README.md index 80bcc5e..390e95f 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Pretty-print tables into ASCII/Unicode strings. - Custom (horizontal) Align per column - Custom (vertical) VAlign per column (and multi-line column support) - Mirror output to an io.Writer object (like os.StdOut) + - Sort by any of the Columns (by Column Name or Number) - Completely customizable styles - Many ready-to-use styles: [table/style.go](table/style.go) - Colorize Headers/Body/Footers using [text/color](text/color.go) diff --git a/progress/tracker.go b/progress/tracker.go index 7edf0e6..7110279 100644 --- a/progress/tracker.go +++ b/progress/tracker.go @@ -2,7 +2,6 @@ package progress import ( "fmt" - "sort" "time" ) @@ -147,85 +146,3 @@ func (tu Units) sprintBytes(value int64) string { } return fmt.Sprintf("%.2fPB", float64(value)/1000000000000000.0) } - -// SortBy helps sort a list of Trackers by various means. -type SortBy int - -const ( - // SortByNone doesn't do any sorting == sort by insertion order. - SortByNone SortBy = iota - - // SortByMessage sorts by the Message alphabetically in ascending order. - SortByMessage - - // SortByMessageDsc sorts by the Message alphabetically in descending order. - SortByMessageDsc - - // SortByPercent sorts by the Percentage complete in ascending order. - SortByPercent - - // SortByPercentDsc sorts by the Percentage complete in descending order. - SortByPercentDsc - - // SortByValue sorts by the Value in ascending order. - SortByValue - - // SortByValueDsc sorts by the Value in descending order. - SortByValueDsc -) - -// Sort applies the sorting method defined by SortBy. -func (ts SortBy) Sort(trackers []*Tracker) { - switch ts { - case SortByMessage: - sort.Sort(sortByMessage(trackers)) - case SortByMessageDsc: - sort.Sort(sortByMessageDsc(trackers)) - case SortByPercent: - sort.Sort(sortByPercent(trackers)) - case SortByPercentDsc: - sort.Sort(sortByPercentDsc(trackers)) - case SortByValue: - sort.Sort(sortByValue(trackers)) - case SortByValueDsc: - sort.Sort(sortByValueDsc(trackers)) - default: - // no sort - } -} - -type sortByMessage []*Tracker - -func (ta sortByMessage) Len() int { return len(ta) } -func (ta sortByMessage) Swap(i, j int) { ta[i], ta[j] = ta[j], ta[i] } -func (ta sortByMessage) Less(i, j int) bool { return ta[i].Message < ta[j].Message } - -type sortByMessageDsc []*Tracker - -func (ta sortByMessageDsc) Len() int { return len(ta) } -func (ta sortByMessageDsc) Swap(i, j int) { ta[i], ta[j] = ta[j], ta[i] } -func (ta sortByMessageDsc) Less(i, j int) bool { return ta[i].Message > ta[j].Message } - -type sortByPercent []*Tracker - -func (ta sortByPercent) Len() int { return len(ta) } -func (ta sortByPercent) Swap(i, j int) { ta[i], ta[j] = ta[j], ta[i] } -func (ta sortByPercent) Less(i, j int) bool { return ta[i].PercentDone() < ta[j].PercentDone() } - -type sortByPercentDsc []*Tracker - -func (ta sortByPercentDsc) Len() int { return len(ta) } -func (ta sortByPercentDsc) Swap(i, j int) { ta[i], ta[j] = ta[j], ta[i] } -func (ta sortByPercentDsc) Less(i, j int) bool { return ta[i].PercentDone() > ta[j].PercentDone() } - -type sortByValue []*Tracker - -func (ta sortByValue) Len() int { return len(ta) } -func (ta sortByValue) Swap(i, j int) { ta[i], ta[j] = ta[j], ta[i] } -func (ta sortByValue) Less(i, j int) bool { return ta[i].value < ta[j].value } - -type sortByValueDsc []*Tracker - -func (ta sortByValueDsc) Len() int { return len(ta) } -func (ta sortByValueDsc) Swap(i, j int) { ta[i], ta[j] = ta[j], ta[i] } -func (ta sortByValueDsc) Less(i, j int) bool { return ta[i].value > ta[j].value } diff --git a/progress/tracker_sort.go b/progress/tracker_sort.go new file mode 100644 index 0000000..0ee4e7a --- /dev/null +++ b/progress/tracker_sort.go @@ -0,0 +1,85 @@ +package progress + +import "sort" + +// SortBy helps sort a list of Trackers by various means. +type SortBy int + +const ( + // SortByNone doesn't do any sorting == sort by insertion order. + SortByNone SortBy = iota + + // SortByMessage sorts by the Message alphabetically in ascending order. + SortByMessage + + // SortByMessageDsc sorts by the Message alphabetically in descending order. + SortByMessageDsc + + // SortByPercent sorts by the Percentage complete in ascending order. + SortByPercent + + // SortByPercentDsc sorts by the Percentage complete in descending order. + SortByPercentDsc + + // SortByValue sorts by the Value in ascending order. + SortByValue + + // SortByValueDsc sorts by the Value in descending order. + SortByValueDsc +) + +// Sort applies the sorting method defined by SortBy. +func (sb SortBy) Sort(trackers []*Tracker) { + switch sb { + case SortByMessage: + sort.Sort(sortByMessage(trackers)) + case SortByMessageDsc: + sort.Sort(sortByMessageDsc(trackers)) + case SortByPercent: + sort.Sort(sortByPercent(trackers)) + case SortByPercentDsc: + sort.Sort(sortByPercentDsc(trackers)) + case SortByValue: + sort.Sort(sortByValue(trackers)) + case SortByValueDsc: + sort.Sort(sortByValueDsc(trackers)) + default: + // no sort + } +} + +type sortByMessage []*Tracker + +func (sb sortByMessage) Len() int { return len(sb) } +func (sb sortByMessage) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] } +func (sb sortByMessage) Less(i, j int) bool { return sb[i].Message < sb[j].Message } + +type sortByMessageDsc []*Tracker + +func (sb sortByMessageDsc) Len() int { return len(sb) } +func (sb sortByMessageDsc) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] } +func (sb sortByMessageDsc) Less(i, j int) bool { return sb[i].Message > sb[j].Message } + +type sortByPercent []*Tracker + +func (sb sortByPercent) Len() int { return len(sb) } +func (sb sortByPercent) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] } +func (sb sortByPercent) Less(i, j int) bool { return sb[i].PercentDone() < sb[j].PercentDone() } + +type sortByPercentDsc []*Tracker + +func (sb sortByPercentDsc) Len() int { return len(sb) } +func (sb sortByPercentDsc) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] } +func (sb sortByPercentDsc) Less(i, j int) bool { return sb[i].PercentDone() > sb[j].PercentDone() } + +type sortByValue []*Tracker + +func (sb sortByValue) Len() int { return len(sb) } +func (sb sortByValue) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] } +func (sb sortByValue) Less(i, j int) bool { return sb[i].value < sb[j].value } + +type sortByValueDsc []*Tracker + +func (sb sortByValueDsc) Len() int { return len(sb) } +func (sb sortByValueDsc) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] } +func (sb sortByValueDsc) Less(i, j int) bool { return sb[i].value > sb[j].value } diff --git a/progress/tracker_sort_test.go b/progress/tracker_sort_test.go new file mode 100644 index 0000000..7a45cbb --- /dev/null +++ b/progress/tracker_sort_test.go @@ -0,0 +1,50 @@ +package progress + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSortBy(t *testing.T) { + trackers := []*Tracker{ + {Message: "Downloading File # 2", Total: 1000, value: 300}, + {Message: "Downloading File # 1", Total: 1000, value: 100}, + {Message: "Downloading File # 3", Total: 1000, value: 500}, + } + + SortByNone.Sort(trackers) + assert.Equal(t, "Downloading File # 2", trackers[0].Message) + assert.Equal(t, "Downloading File # 1", trackers[1].Message) + assert.Equal(t, "Downloading File # 3", trackers[2].Message) + + SortByMessage.Sort(trackers) + assert.Equal(t, "Downloading File # 1", trackers[0].Message) + assert.Equal(t, "Downloading File # 2", trackers[1].Message) + assert.Equal(t, "Downloading File # 3", trackers[2].Message) + + SortByMessageDsc.Sort(trackers) + assert.Equal(t, "Downloading File # 3", trackers[0].Message) + assert.Equal(t, "Downloading File # 2", trackers[1].Message) + assert.Equal(t, "Downloading File # 1", trackers[2].Message) + + SortByPercent.Sort(trackers) + assert.Equal(t, "Downloading File # 1", trackers[0].Message) + assert.Equal(t, "Downloading File # 2", trackers[1].Message) + assert.Equal(t, "Downloading File # 3", trackers[2].Message) + + SortByPercentDsc.Sort(trackers) + assert.Equal(t, "Downloading File # 3", trackers[0].Message) + assert.Equal(t, "Downloading File # 2", trackers[1].Message) + assert.Equal(t, "Downloading File # 1", trackers[2].Message) + + SortByValue.Sort(trackers) + assert.Equal(t, "Downloading File # 1", trackers[0].Message) + assert.Equal(t, "Downloading File # 2", trackers[1].Message) + assert.Equal(t, "Downloading File # 3", trackers[2].Message) + + SortByValueDsc.Sort(trackers) + assert.Equal(t, "Downloading File # 3", trackers[0].Message) + assert.Equal(t, "Downloading File # 2", trackers[1].Message) + assert.Equal(t, "Downloading File # 1", trackers[2].Message) +} diff --git a/progress/tracker_test.go b/progress/tracker_test.go index b644327..4a699c8 100644 --- a/progress/tracker_test.go +++ b/progress/tracker_test.go @@ -2,9 +2,9 @@ package progress import ( "testing" + "time" "github.com/stretchr/testify/assert" - "time" ) func TestTracker_Increment(t *testing.T) { @@ -114,46 +114,3 @@ func TestUnits_Sprint(t *testing.T) { assert.Equal(t, "£1.50Q", UnitsCurrencyPound.Sprint(1500000000000000)) assert.Equal(t, "£1500.00Q", UnitsCurrencyPound.Sprint(1500000000000000000)) } - -func TestSortBy(t *testing.T) { - trackers := []*Tracker{ - {Message: "Downloading File # 2", Total: 1000, value: 300}, - {Message: "Downloading File # 1", Total: 1000, value: 100}, - {Message: "Downloading File # 3", Total: 1000, value: 500}, - } - - SortByNone.Sort(trackers) - assert.Equal(t, "Downloading File # 2", trackers[0].Message) - assert.Equal(t, "Downloading File # 1", trackers[1].Message) - assert.Equal(t, "Downloading File # 3", trackers[2].Message) - - SortByMessage.Sort(trackers) - assert.Equal(t, "Downloading File # 1", trackers[0].Message) - assert.Equal(t, "Downloading File # 2", trackers[1].Message) - assert.Equal(t, "Downloading File # 3", trackers[2].Message) - - SortByMessageDsc.Sort(trackers) - assert.Equal(t, "Downloading File # 3", trackers[0].Message) - assert.Equal(t, "Downloading File # 2", trackers[1].Message) - assert.Equal(t, "Downloading File # 1", trackers[2].Message) - - SortByPercent.Sort(trackers) - assert.Equal(t, "Downloading File # 1", trackers[0].Message) - assert.Equal(t, "Downloading File # 2", trackers[1].Message) - assert.Equal(t, "Downloading File # 3", trackers[2].Message) - - SortByPercentDsc.Sort(trackers) - assert.Equal(t, "Downloading File # 3", trackers[0].Message) - assert.Equal(t, "Downloading File # 2", trackers[1].Message) - assert.Equal(t, "Downloading File # 1", trackers[2].Message) - - SortByValue.Sort(trackers) - assert.Equal(t, "Downloading File # 1", trackers[0].Message) - assert.Equal(t, "Downloading File # 2", trackers[1].Message) - assert.Equal(t, "Downloading File # 3", trackers[2].Message) - - SortByValueDsc.Sort(trackers) - assert.Equal(t, "Downloading File # 3", trackers[0].Message) - assert.Equal(t, "Downloading File # 2", trackers[1].Message) - assert.Equal(t, "Downloading File # 1", trackers[2].Message) -} diff --git a/table/README.md b/table/README.md index fc2cdab..6828dcc 100644 --- a/table/README.md +++ b/table/README.md @@ -12,6 +12,7 @@ Pretty-print tables into ASCII/Unicode strings. - Custom (horizontal) Align per column - Custom (vertical) VAlign per column (and multi-line column support) - Mirror output to an io.Writer object (like os.StdOut) + - Sort by any of the Columns (by Column Name or Number) - Completely customizable styles - Many ready-to-use styles: [style.go](style.go) - Colorize Headers/Body/Footers using [../text/color.go](../text/color.go) diff --git a/table/render.go b/table/render.go index d6661db..b27a3db 100644 --- a/table/render.go +++ b/table/render.go @@ -252,8 +252,13 @@ func (t *Table) renderRow(out *strings.Builder, rowNum int, row rowStr, colors [ } func (t *Table) renderRows(out *strings.Builder, rows []rowStr, colors []text.Colors, format text.Format, hint renderHint) { - for idx, row := range rows { - t.renderRow(out, idx+1, row, colors, format, hint) + for idx := range rows { + sortedIdx := idx + if hint.isRegularRow() { + sortedIdx = t.sortedRowIndices[idx] + } + + t.renderRow(out, idx+1, rows[sortedIdx], colors, format, hint) if t.style.Options.SeparateRows && idx < len(rows)-1 { t.renderRowSeparator(out, hint) } diff --git a/table/render_test.go b/table/render_test.go index b43ed03..858c9cc 100644 --- a/table/render_test.go +++ b/table/render_test.go @@ -320,6 +320,29 @@ func TestTable_Render_Empty(t *testing.T) { assert.Empty(t, tw.Render()) } +func TestTable_Render_Sorted(t *testing.T) { + tw := NewWriter() + tw.AppendHeader(testHeader) + tw.AppendRows(testRows) + tw.AppendRow(Row{11, "Sansa", "Stark", 6000}) + tw.AppendFooter(testFooter) + tw.SetStyle(StyleLight) + tw.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}}) + + expectedOut := `┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ +│ # │ 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 │ │ +└─────┴────────────┴───────────┴────────┴─────────────────────────────┘` + fmt.Println(tw.Render()) + assert.Equal(t, expectedOut, tw.Render()) +} + func TestTable_Render_TableWithinTable(t *testing.T) { twInner := NewWriter() twInner.AppendHeader(testHeader) diff --git a/table/sort.go b/table/sort.go new file mode 100644 index 0000000..189991f --- /dev/null +++ b/table/sort.go @@ -0,0 +1,120 @@ +package table + +import ( + "sort" + "strconv" +) + +// SortBy defines What to sort (Column Name or Number), and How to sort (Mode). +type SortBy struct { + // Name is the name of the Column as it appears in the first Header row. + // If a Header is not provided, or the name is not found in the header, this + // will not work. + Name string + + // Number is the Column # from left. When specified, it overrides the Name + // property. If you know the exact Column number, use this instead of Name. + Number int + + // Mode tells the Writer how to Sort. Asc/Dsc/etc. + Mode SortMode +} + +// SortMode defines How to sort. +type SortMode int + +const ( + // Asc sorts the column in Ascending order alphabetically. + Asc SortMode = iota + // AscNumeric sorts the column in Ascending order numerically. + AscNumeric + // Dsc sorts the column in Descending order alphabetically. + Dsc + // DscNumeric sorts the column in Descending order numerically. + DscNumeric +) + +type rowsSorter struct { + rows []rowStr + sortBy []SortBy + sortedIndices []int +} + +// sortRows sorts and returns the row indices in Sorted order as directed by +// Table.sortBy which can be set using Table.SortBy(...) +func (t *Table) sortRows(rows []rowStr) []int { + sortedIndices := make([]int, len(rows)) + for idx := range rows { + sortedIndices[idx] = idx + } + + if t.sortBy != nil && len(t.sortBy) > 0 { + sort.Sort(rowsSorter{ + rows: rows, + sortBy: t.parseSortBy(t.sortBy), + sortedIndices: sortedIndices, + }) + } + + return sortedIndices +} + +func (t *Table) parseSortBy(sortBy []SortBy) []SortBy { + var resSortBy []SortBy + for _, col := range sortBy { + colNum := 0 + if col.Number > 0 && col.Number <= t.numColumns { + colNum = col.Number + } else if col.Name != "" && len(t.rowsHeader) > 0 { + for idx, colName := range t.rowsHeader[0] { + if col.Name == colName { + colNum = idx + 1 + break + } + } + } + if colNum > 0 { + resSortBy = append(resSortBy, SortBy{ + Name: col.Name, + Number: colNum, + Mode: col.Mode, + }) + } + } + return resSortBy +} + +func (rs rowsSorter) Len() int { + return len(rs.rows) +} + +func (rs rowsSorter) Swap(i, j int) { + rs.sortedIndices[i], rs.sortedIndices[j] = rs.sortedIndices[j], rs.sortedIndices[i] +} + +func (rs rowsSorter) Less(i, j int) bool { + realI, realJ := rs.sortedIndices[i], rs.sortedIndices[j] + for _, col := range rs.sortBy { + rowI, rowJ, colIdx := rs.rows[realI], rs.rows[realJ], col.Number-1 + if colIdx < len(rowI) && colIdx < len(rowJ) { + if rowI[colIdx] == rowJ[colIdx] { + continue + } else if col.Mode == Asc { + return rowI[colIdx] < rowJ[colIdx] + } else if col.Mode == Dsc { + return rowI[colIdx] > rowJ[colIdx] + } else { + iVal, iErr := strconv.ParseFloat(rowI[colIdx], 64) + jVal, jErr := strconv.ParseFloat(rowJ[colIdx], 64) + if iErr == nil && jErr == nil { + if col.Mode == AscNumeric { + return iVal < jVal + } else if col.Mode == DscNumeric { + return jVal < iVal + } + } + } + } + } + return false +} diff --git a/table/sort_test.go b/table/sort_test.go new file mode 100644 index 0000000..f6ec6a0 --- /dev/null +++ b/table/sort_test.go @@ -0,0 +1,123 @@ +package table + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestTable_sortRows_WithName(t *testing.T) { + table := Table{} + table.AppendHeader(Row{"#", "First Name", "Last Name", "Salary"}) + table.AppendRows([]Row{ + {1, "Arya", "Stark", 3000}, + {11, "Sansa", "Stark", 3000}, + {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, + {300, "Tyrion", "Lannister", 5000}, + }) + assert.Equal(t, []int{0, 1, 2, 3}, table.sortRows(table.rows)) + + // sort by "#" + table.SortBy([]SortBy{{Name: "#", Mode: AscNumeric}}) + assert.Equal(t, []int{0, 1, 2, 3}, table.sortRows(table.rows)) + + table.SortBy([]SortBy{{Name: "#", Mode: DscNumeric}}) + assert.Equal(t, []int{3, 2, 1, 0}, table.sortRows(table.rows)) + + // sort by First Name, Last Name + table.SortBy([]SortBy{{Name: "First Name", Mode: Asc}, {Name: "Last Name", Mode: Asc}}) + assert.Equal(t, []int{0, 2, 1, 3}, table.sortRows(table.rows)) + + table.SortBy([]SortBy{{Name: "First Name", Mode: Asc}, {Name: "Last Name", Mode: Dsc}}) + assert.Equal(t, []int{0, 2, 1, 3}, table.sortRows(table.rows)) + + table.SortBy([]SortBy{{Name: "First Name", Mode: Dsc}, {Name: "Last Name", Mode: Asc}}) + assert.Equal(t, []int{3, 1, 2, 0}, table.sortRows(table.rows)) + + table.SortBy([]SortBy{{Name: "First Name", Mode: Dsc}, {Name: "Last Name", Mode: Dsc}}) + assert.Equal(t, []int{3, 1, 2, 0}, table.sortRows(table.rows)) + + // sort by Last Name, First Name + table.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}}) + assert.Equal(t, []int{3, 2, 0, 1}, table.sortRows(table.rows)) + + table.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Dsc}}) + assert.Equal(t, []int{3, 2, 1, 0}, table.sortRows(table.rows)) + + table.SortBy([]SortBy{{Name: "Last Name", Mode: Dsc}, {Name: "First Name", Mode: Asc}}) + assert.Equal(t, []int{0, 1, 2, 3}, table.sortRows(table.rows)) + + table.SortBy([]SortBy{{Name: "Last Name", Mode: Dsc}, {Name: "First Name", Mode: Dsc}}) + assert.Equal(t, []int{1, 0, 2, 3}, table.sortRows(table.rows)) + + // sort by Unknown Column + table.SortBy([]SortBy{{Name: "Last Name", Mode: Dsc}, {Name: "Foo Bar", Mode: Dsc}}) + assert.Equal(t, []int{0, 1, 2, 3}, table.sortRows(table.rows)) + + // sort by Salary + table.SortBy([]SortBy{{Name: "Salary", Mode: AscNumeric}}) + assert.Equal(t, []int{2, 0, 1, 3}, table.sortRows(table.rows)) + + table.SortBy([]SortBy{{Name: "Salary", Mode: DscNumeric}}) + assert.Equal(t, []int{3, 0, 1, 2}, table.sortRows(table.rows)) + + table.SortBy(nil) + assert.Equal(t, []int{0, 1, 2, 3}, table.sortRows(table.rows)) +} + +func TestTable_sortRows_WithoutName(t *testing.T) { + table := Table{} + table.AppendRows([]Row{ + {1, "Arya", "Stark", 3000}, + {11, "Sansa", "Stark", 3000}, + {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, + {300, "Tyrion", "Lannister", 5000}, + }) + assert.Equal(t, []int{0, 1, 2, 3}, table.sortRows(table.rows)) + + // sort by "#" + table.SortBy([]SortBy{{Number: 1, Mode: AscNumeric}}) + assert.Equal(t, []int{0, 1, 2, 3}, table.sortRows(table.rows)) + + table.SortBy([]SortBy{{Number: 1, Mode: DscNumeric}}) + assert.Equal(t, []int{3, 2, 1, 0}, table.sortRows(table.rows)) + + // sort by First Name, Last Name + table.SortBy([]SortBy{{Number: 2, Mode: Asc}, {Number: 3, Mode: Asc}}) + assert.Equal(t, []int{0, 2, 1, 3}, table.sortRows(table.rows)) + + table.SortBy([]SortBy{{Number: 2, Mode: Asc}, {Number: 3, Mode: Dsc}}) + assert.Equal(t, []int{0, 2, 1, 3}, table.sortRows(table.rows)) + + table.SortBy([]SortBy{{Number: 2, Mode: Dsc}, {Number: 3, Mode: Asc}}) + assert.Equal(t, []int{3, 1, 2, 0}, table.sortRows(table.rows)) + + table.SortBy([]SortBy{{Number: 2, Mode: Dsc}, {Number: 3, Mode: Dsc}}) + assert.Equal(t, []int{3, 1, 2, 0}, table.sortRows(table.rows)) + + // sort by Last Name, First Name + table.SortBy([]SortBy{{Number: 3, Mode: Asc}, {Number: 2, Mode: Asc}}) + assert.Equal(t, []int{3, 2, 0, 1}, table.sortRows(table.rows)) + + table.SortBy([]SortBy{{Number: 3, Mode: Asc}, {Number: 2, Mode: Dsc}}) + assert.Equal(t, []int{3, 2, 1, 0}, table.sortRows(table.rows)) + + table.SortBy([]SortBy{{Number: 3, Mode: Dsc}, {Number: 2, Mode: Asc}}) + assert.Equal(t, []int{0, 1, 2, 3}, table.sortRows(table.rows)) + + table.SortBy([]SortBy{{Number: 3, Mode: Dsc}, {Number: 2, Mode: Dsc}}) + assert.Equal(t, []int{1, 0, 2, 3}, table.sortRows(table.rows)) + + // sort by Unknown Column + table.SortBy([]SortBy{{Number: 3, Mode: Dsc}, {Number: 99, Mode: Dsc}}) + assert.Equal(t, []int{0, 1, 2, 3}, table.sortRows(table.rows)) + + // sort by Salary + table.SortBy([]SortBy{{Number: 4, Mode: AscNumeric}}) + assert.Equal(t, []int{2, 0, 1, 3}, table.sortRows(table.rows)) + + table.SortBy([]SortBy{{Number: 4, Mode: DscNumeric}}) + assert.Equal(t, []int{3, 0, 1, 2}, table.sortRows(table.rows)) + + table.SortBy(nil) + assert.Equal(t, []int{0, 1, 2, 3}, table.sortRows(table.rows)) +} diff --git a/table/table.go b/table/table.go index f210dcb..5d853d7 100644 --- a/table/table.go +++ b/table/table.go @@ -64,6 +64,11 @@ type Table struct { // rowSeparator is a dummy row that contains the separator columns (dashes // that make up the separator between header/body/footer rowSeparator rowStr + // sortBy stores a map of Column + sortBy []SortBy + // sortedRowIndices is used to temporarily store the order of rows after + // sorting as defined by the sortBy + sortedRowIndices []int // style contains all the strings used to draw the table, and more style *Style // vAlign describes the vertical-align for each column @@ -166,7 +171,7 @@ func (t *Table) SetHTMLCSSClass(cssClass string) { } // SetIndexColumn sets the given Column # as the column that has the row -// "Index". Valid values range from 1 to N. Note that this is not 0-indexed. +// "Number". Valid values range from 1 to N. Note that this is not 0-indexed. func (t *Table) SetIndexColumn(colNum int) { t.indexColumn = colNum } @@ -197,6 +202,13 @@ func (t *Table) SetVAlignHeader(vAlign []text.VAlign) { t.vAlignHeader = vAlign } +// SortBy sets the rules for sorting the Rows in the order specified. i.e., the +// first SortBy instruction takes precedence over the second and so on. Any +// duplicate instructions on the same column will be discarded while sorting. +func (t *Table) SortBy(sortBy []SortBy) { + t.sortBy = sortBy +} + // Style returns the current style. func (t *Table) Style() *Style { if t.style == nil { @@ -206,20 +218,6 @@ func (t *Table) Style() *Style { return t.style } -// renderHint has hints for the Render*() logic -type renderHint struct { - isAutoIndexColumn bool - isFirstRow bool - isFooterRow bool - isHeaderRow bool - isLastRow bool - isSeparatorRow bool -} - -func (h *renderHint) isRegularRow() bool { - return !h.isHeaderRow && !h.isFooterRow -} - func (t *Table) analyzeAndStringify(row Row, isHeader bool, isFooter bool) rowStr { // update t.numColumns if this row is the longest seen till now if len(row) > t.numColumns { @@ -313,6 +311,9 @@ func (t *Table) initForRender() { // generate a separator row and calculate maximum row length t.initForRenderRowSeparator() + + // sort and get the indices for the rows in sort order + t.sortedRowIndices = t.sortRows(t.rows) } func (t *Table) initForRenderMaxColumnLength() { @@ -359,3 +360,17 @@ func (t *Table) render(out *strings.Builder) string { } return outStr } + +// renderHint has hints for the Render*() logic +type renderHint struct { + isAutoIndexColumn bool + isFirstRow bool + isFooterRow bool + isHeaderRow bool + isLastRow bool + isSeparatorRow bool +} + +func (h *renderHint) isRegularRow() bool { + return !h.isHeaderRow && !h.isFooterRow +} diff --git a/table/table_test.go b/table/table_test.go index 4c18c5b..dfc8e0d 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -351,6 +351,17 @@ func TestTable_SetOutputMirror(t *testing.T) { assert.Equal(t, expectedOut+"\n", mockOutputMirror.mirroredOutput) } +func TestTable_SortByColumn(t *testing.T) { + table := Table{} + assert.Empty(t, table.sortBy) + + table.SortBy([]SortBy{{Name: "#", Mode: Asc}}) + assert.Equal(t, 1, len(table.sortBy)) + + table.SortBy([]SortBy{{Name: "First Name", Mode: Dsc}, {Name: "Last Name", Mode: Asc}}) + assert.Equal(t, 2, len(table.sortBy)) +} + func TestTable_SetVAlign(t *testing.T) { table := Table{} assert.Nil(t, table.vAlign) diff --git a/table/writer.go b/table/writer.go index 8094317..5b2c8f8 100644 --- a/table/writer.go +++ b/table/writer.go @@ -34,6 +34,7 @@ type Writer interface { SetVAlign(vAlign []text.VAlign) SetVAlignFooter(vAlign []text.VAlign) SetVAlignHeader(vAlign []text.VAlign) + SortBy(sortBy []SortBy) Style() *Style } diff --git a/util/auto_index.go b/util/auto_index.go index b2da900..46f4ed1 100644 --- a/util/auto_index.go +++ b/util/auto_index.go @@ -1,6 +1,6 @@ package util -// AutoIndexColumnID returns a unique Column ID/Name for the given Column Index. +// AutoIndexColumnID returns a unique Column ID/Name for the given Column Number. // The functionality is similar to what you get in an Excel spreadsheet w.r.t. // the Column ID/Name. func AutoIndexColumnID(colIdx int) string {