Skip to content

Commit

Permalink
table: allow alphanumerical sort to be case-insensitive (#309)
Browse files Browse the repository at this point in the history
  • Loading branch information
Skeeve committed Mar 15, 2024
1 parent 98472a2 commit b9fd422
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 26 deletions.
11 changes: 11 additions & 0 deletions table/render_test.go
Expand Up @@ -961,6 +961,17 @@ func TestTable_Render_Sorted(t *testing.T) {
│ 11 │ Sansa │ Stark │ 6000 │ │
├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
│ │ │ TOTAL │ 10000 │ │
└─────┴────────────┴───────────┴────────┴─────────────────────────────┘`)
tw.SortBy([]SortBy{{Number: 5, Mode: Dsc}, {Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}})
compareOutput(t, tw.Render(), `┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
│ # │ FIRST NAME │ LAST NAME │ SALARY │ │
├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
│ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
│ 300 │ Tyrion │ Lannister │ 5000 │ │
│ 1 │ Arya │ Stark │ 3000 │ │
│ 11 │ Sansa │ Stark │ 6000 │ │
├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
│ │ │ TOTAL │ 10000 │ │
└─────┴────────────┴───────────┴────────┴─────────────────────────────┘`)
}

Expand Down
71 changes: 45 additions & 26 deletions table/sort.go
Expand Up @@ -3,6 +3,7 @@ package table
import (
"sort"
"strconv"
"strings"
)

// SortBy defines What to sort (Column Name or Number), and How to sort (Mode).
Expand All @@ -17,6 +18,9 @@ type SortBy struct {

// Mode tells the Writer how to Sort. Asc/Dsc/etc.
Mode SortMode

// IgnoreCase makes sorting case-insensitive
IgnoreCase bool
}

// SortMode defines How to sort.
Expand Down Expand Up @@ -86,9 +90,10 @@ func (t *Table) parseSortBy(sortBy []SortBy) []SortBy {
}
if colNum > 0 {
resSortBy = append(resSortBy, SortBy{
Name: col.Name,
Number: colNum,
Mode: col.Mode,
Name: col.Name,
Number: colNum,
Mode: col.Mode,
IgnoreCase: col.IgnoreCase,
})
}
}
Expand All @@ -105,7 +110,8 @@ func (rs rowsSorter) Swap(i, j int) {

func (rs rowsSorter) Less(i, j int) bool {
realI, realJ := rs.sortedIndices[i], rs.sortedIndices[j]
for _, sortBy := range rs.sortBy {
lastSort := len(rs.sortBy) - 1
for sortIdx, sortBy := range rs.sortBy {
// extract the values/cells from the rows for comparison
rowI, rowJ, colIdx := rs.rows[realI], rs.rows[realJ], sortBy.Number-1
iVal, jVal := "", ""
Expand All @@ -117,90 +123,103 @@ func (rs rowsSorter) Less(i, j int) bool {
}

// compare and choose whether to continue
shouldContinue, returnValue := less(iVal, jVal, sortBy.Mode)
if !shouldContinue {
shouldContinue, returnValue := less(iVal, jVal, sortBy)
if !shouldContinue || lastSort == sortIdx {
return returnValue
}
}
return false
}

func less(iVal string, jVal string, mode SortMode) (bool, bool) {
func less(iVal string, jVal string, sb SortBy) (bool, bool) {
if iVal == jVal {
return true, false
}

switch mode {
switch sb.Mode {
case Asc, Dsc:
return lessAlphabetic(iVal, jVal, mode)
return lessAlphabetic(iVal, jVal, sb)
case AscNumeric, DscNumeric:
return lessNumeric(iVal, jVal, mode)
return lessNumeric(iVal, jVal, sb)
default: // AscAlphaNumeric, AscNumericAlpha, DscAlphaNumeric, DscNumericAlpha
return lessMixedMode(iVal, jVal, mode)
return lessMixedMode(iVal, jVal, sb)
}
}

func lessAlphabetic(iVal string, jVal string, mode SortMode) (bool, bool) {
switch mode {
func lessAlphabetic(iVal string, jVal string, sb SortBy) (bool, bool) {
if sb.IgnoreCase {
iLow := strings.ToLower(iVal)
jLow := strings.ToLower(jVal)
// when two strings are case-insensitive identical, compare them casesensitive.
// That makes sure to get a consistent sorting
identical := iLow == jLow
switch sb.Mode {
case Asc, AscAlphaNumeric, AscNumericAlpha:
return identical, (identical && iVal < jVal) || iLow < jLow
default: // Dsc, DscAlphaNumeric, DscNumericAlpha
return identical, (identical && iVal > jVal) || iLow > jLow
}
}
switch sb.Mode {
case Asc, AscAlphaNumeric, AscNumericAlpha:
return false, iVal < jVal
default: // Dsc, DscAlphaNumeric, DscNumericAlpha
return false, iVal > jVal
}
}

func lessAlphaNumericI(mode SortMode) (bool, bool) {
func lessAlphaNumericI(sb SortBy) (bool, bool) {
// i == "abc"; j == 5
switch mode {
switch sb.Mode {
case AscAlphaNumeric, DscAlphaNumeric:
return false, true
default: // AscNumericAlpha, DscNumericAlpha
return false, false
}
}

func lessAlphaNumericJ(mode SortMode) (bool, bool) {
func lessAlphaNumericJ(sb SortBy) (bool, bool) {
// i == 5; j == "abc"
switch mode {
switch sb.Mode {
case AscAlphaNumeric, DscAlphaNumeric:
return false, false
default: // AscNumericAlpha, DscNumericAlpha:
return false, true
}
}

func lessMixedMode(iVal string, jVal string, mode SortMode) (bool, bool) {
func lessMixedMode(iVal string, jVal string, sb SortBy) (bool, bool) {
iNumVal, iErr := strconv.ParseFloat(iVal, 64)
jNumVal, jErr := strconv.ParseFloat(jVal, 64)
if iErr != nil && jErr != nil { // both are alphanumeric
return lessAlphabetic(iVal, jVal, mode)
return lessAlphabetic(iVal, jVal, sb)
}
if iErr != nil { // iVal is alphabetic, jVal is numeric
return lessAlphaNumericI(mode)
return lessAlphaNumericI(sb)
}
if jErr != nil { // iVal is numeric, jVal is alphabetic
return lessAlphaNumericJ(mode)
return lessAlphaNumericJ(sb)
}
// both values numeric
return lessNumericVal(iNumVal, jNumVal, mode)
return lessNumericVal(iNumVal, jNumVal, sb)
}

func lessNumeric(iVal string, jVal string, mode SortMode) (bool, bool) {
func lessNumeric(iVal string, jVal string, sb SortBy) (bool, bool) {
iNumVal, iErr := strconv.ParseFloat(iVal, 64)
jNumVal, jErr := strconv.ParseFloat(jVal, 64)
if iErr != nil || jErr != nil {
return false, false
}

return lessNumericVal(iNumVal, jNumVal, mode)
return lessNumericVal(iNumVal, jNumVal, sb)
}

func lessNumericVal(iVal float64, jVal float64, mode SortMode) (bool, bool) {
func lessNumericVal(iVal float64, jVal float64, sb SortBy) (bool, bool) {
if iVal == jVal {
return true, false
}

switch mode {
switch sb.Mode {
case AscNumeric, AscAlphaNumeric, AscNumericAlpha:
return false, iVal < jVal
default: // DscNumeric, DscAlphaNumeric, DscNumericAlpha
Expand Down
86 changes: 86 additions & 0 deletions table/table_test.go
Expand Up @@ -290,6 +290,92 @@ func TestTable_SetAutoIndex(t *testing.T) {
│ │ │ │ │ │ This is known. │
└───┴─────┴────────────┴───────────┴────────┴─────────────────────────────┘`
assert.Equal(t, expectedOut, table.Render())
tw := NewWriter()
tw.AppendHeader(Row{"#", "Name", "Prefix", "Number", "Class"})
tw.SetColumnConfigs([]ColumnConfig{
{Number: 1, Align: text.AlignRight, AlignHeader: text.AlignCenter},
{Number: 2, Align: text.AlignAuto, AlignHeader: text.AlignCenter},
{Number: 3, Align: text.AlignAuto, AlignHeader: text.AlignCenter},
{Number: 4, Align: text.AlignAuto, AlignHeader: text.AlignCenter},
{Number: 5, Align: text.AlignAuto, AlignHeader: text.AlignCenter},
})
tw.AppendRows([]Row{
{0, "defiant", "NCC", 1764, "Constitution"},
{1, "Defiant", "nx", 74205, "Defiant"},
{2, "entente", "ncc", 2120, "Dreadnought"},
{3, "Enterprise", "NCC", 1701, "Constitution"},
{4, "Farragut", "NCC", 1647, "(Farragut-Type)"},
{5, "farragut", "NCC", 60597, "Nebula"},
{6, "Bonaventure", "", "10283NCC", "(Bonaventure-Typ)"},
{7, "IKS Ch'Tang", "", "-----------", "Bird-of-Prey"},
{8, "IKS Drovana", "", "-----------", "Vor'cha-Klasse"},
{9, "IKS Buruk", "", "-----------", "Bird-of-Prey"},
})
tw.SetStyle(StyleLight)
tw.SortBy([]SortBy{{Name: "Name", Mode: Asc, IgnoreCase: false}})
assert.Equal(t, `┌───┬─────────────┬────────┬─────────────┬───────────────────┐
│ # │ NAME │ PREFIX │ NUMBER │ CLASS │
├───┼─────────────┼────────┼─────────────┼───────────────────┤
│ 6 │ Bonaventure │ │ 10283NCC │ (Bonaventure-Typ) │
│ 1 │ Defiant │ nx │ 74205 │ Defiant │
│ 3 │ Enterprise │ NCC │ 1701 │ Constitution │
│ 4 │ Farragut │ NCC │ 1647 │ (Farragut-Type) │
│ 9 │ IKS Buruk │ │ ----------- │ Bird-of-Prey │
│ 7 │ IKS Ch'Tang │ │ ----------- │ Bird-of-Prey │
│ 8 │ IKS Drovana │ │ ----------- │ Vor'cha-Klasse │
│ 0 │ defiant │ NCC │ 1764 │ Constitution │
│ 2 │ entente │ ncc │ 2120 │ Dreadnought │
│ 5 │ farragut │ NCC │ 60597 │ Nebula │
└───┴─────────────┴────────┴─────────────┴───────────────────┘`, tw.Render())

tw.SortBy([]SortBy{{Name: "Name", Mode: Asc, IgnoreCase: true}})
assert.Equal(t, `┌───┬─────────────┬────────┬─────────────┬───────────────────┐
│ # │ NAME │ PREFIX │ NUMBER │ CLASS │
├───┼─────────────┼────────┼─────────────┼───────────────────┤
│ 6 │ Bonaventure │ │ 10283NCC │ (Bonaventure-Typ) │
│ 1 │ Defiant │ nx │ 74205 │ Defiant │
│ 0 │ defiant │ NCC │ 1764 │ Constitution │
│ 2 │ entente │ ncc │ 2120 │ Dreadnought │
│ 3 │ Enterprise │ NCC │ 1701 │ Constitution │
│ 4 │ Farragut │ NCC │ 1647 │ (Farragut-Type) │
│ 5 │ farragut │ NCC │ 60597 │ Nebula │
│ 9 │ IKS Buruk │ │ ----------- │ Bird-of-Prey │
│ 7 │ IKS Ch'Tang │ │ ----------- │ Bird-of-Prey │
│ 8 │ IKS Drovana │ │ ----------- │ Vor'cha-Klasse │
└───┴─────────────┴────────┴─────────────┴───────────────────┘`, tw.Render())

tw.SortBy([]SortBy{{Name: "Prefix", Mode: Asc, IgnoreCase: true}, {Name: "Number", Mode: AscNumericAlpha}})
assert.Equal(t, `┌───┬─────────────┬────────┬─────────────┬───────────────────┐
│ # │ NAME │ PREFIX │ NUMBER │ CLASS │
├───┼─────────────┼────────┼─────────────┼───────────────────┤
│ 7 │ IKS Ch'Tang │ │ ----------- │ Bird-of-Prey │
│ 8 │ IKS Drovana │ │ ----------- │ Vor'cha-Klasse │
│ 9 │ IKS Buruk │ │ ----------- │ Bird-of-Prey │
│ 6 │ Bonaventure │ │ 10283NCC │ (Bonaventure-Typ) │
│ 4 │ Farragut │ NCC │ 1647 │ (Farragut-Type) │
│ 3 │ Enterprise │ NCC │ 1701 │ Constitution │
│ 0 │ defiant │ NCC │ 1764 │ Constitution │
│ 2 │ entente │ ncc │ 2120 │ Dreadnought │
│ 5 │ farragut │ NCC │ 60597 │ Nebula │
│ 1 │ Defiant │ nx │ 74205 │ Defiant │
└───┴─────────────┴────────┴─────────────┴───────────────────┘`, tw.Render())

tw.SortBy([]SortBy{{Name: "Number", Mode: AscNumericAlpha}, {Name: "Name", Mode: Asc}})
assert.Equal(t, `┌───┬─────────────┬────────┬─────────────┬───────────────────┐
│ # │ NAME │ PREFIX │ NUMBER │ CLASS │
├───┼─────────────┼────────┼─────────────┼───────────────────┤
│ 4 │ Farragut │ NCC │ 1647 │ (Farragut-Type) │
│ 3 │ Enterprise │ NCC │ 1701 │ Constitution │
│ 0 │ defiant │ NCC │ 1764 │ Constitution │
│ 2 │ entente │ ncc │ 2120 │ Dreadnought │
│ 5 │ farragut │ NCC │ 60597 │ Nebula │
│ 1 │ Defiant │ nx │ 74205 │ Defiant │
│ 9 │ IKS Buruk │ │ ----------- │ Bird-of-Prey │
│ 7 │ IKS Ch'Tang │ │ ----------- │ Bird-of-Prey │
│ 8 │ IKS Drovana │ │ ----------- │ Vor'cha-Klasse │
│ 6 │ Bonaventure │ │ 10283NCC │ (Bonaventure-Typ) │
└───┴─────────────┴────────┴─────────────┴───────────────────┘`, tw.Render())

}

func TestTable_SetCaption(t *testing.T) {
Expand Down

0 comments on commit b9fd422

Please sign in to comment.