From b9fd4225364881b1e40be72d4d3d1eb7f2d18d0d Mon Sep 17 00:00:00 2001 From: Stephan Hradek Date: Fri, 15 Mar 2024 15:36:47 +0100 Subject: [PATCH] table: allow alphanumerical sort to be case-insensitive (#309) --- table/render_test.go | 11 ++++++ table/sort.go | 71 ++++++++++++++++++++++-------------- table/table_test.go | 86 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 26 deletions(-) diff --git a/table/render_test.go b/table/render_test.go index 4dc2e26..b11bdd0 100644 --- a/table/render_test.go +++ b/table/render_test.go @@ -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 │ │ └─────┴────────────┴───────────┴────────┴─────────────────────────────┘`) } diff --git a/table/sort.go b/table/sort.go index ad229d6..51520bb 100644 --- a/table/sort.go +++ b/table/sort.go @@ -3,6 +3,7 @@ package table import ( "sort" "strconv" + "strings" ) // SortBy defines What to sort (Column Name or Number), and How to sort (Mode). @@ -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. @@ -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, }) } } @@ -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 := "", "" @@ -117,31 +123,44 @@ 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 @@ -149,9 +168,9 @@ func lessAlphabetic(iVal string, jVal string, mode SortMode) (bool, bool) { } } -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 @@ -159,9 +178,9 @@ func lessAlphaNumericI(mode SortMode) (bool, bool) { } } -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: @@ -169,38 +188,38 @@ func lessAlphaNumericJ(mode SortMode) (bool, bool) { } } -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 diff --git a/table/table_test.go b/table/table_test.go index 9addf88..bfe23e9 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -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) {