Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow alphanumerical sort to be case-insensitive #309

Merged
merged 1 commit into from Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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