Repository: jedib0t/go-pretty
Pull request #309 (2024-03-15 09:17:48)

Allow alphanumerical sort to be case-insensitive
## Proposed Changes

Allow table sorting to ignore case.

sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=jedib0t_go-pretty&pullRequest=309) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=jedib0t_go-pretty&pullRequest=309&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/component_measures?id=jedib0t_go-pretty&pullRequest=309&metric=new_accepted_issues&view=list)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=jedib0t_go-pretty&pullRequest=309&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=jedib0t_go-pretty&pullRequest=309&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=jedib0t_go-pretty&pullRequest=309)


coveralls: ## Pull Request Test Coverage Report for [Build 8293920125](https://coveralls.io/builds/66311105)


### Details

* **37** of **39**   **(94.87%)**  changed or added relevant lines in **1** file are covered.
* **1** unchanged line in **1** file lost coverage.
* Overall coverage decreased (**-0.09%**) to **99.914%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [table/sort.go](https://coveralls.io/builds/66311105/source?filename=table%2Fsort.go#L159) | 37 | 39 | 94.87%
<!-- | **Total:** | **37** | **39** | **94.87%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [table/sort.go](https://coveralls.io/builds/66311105/source?filename=table%2Fsort.go#L131) | 1 | 97.66% |
<!-- | **Total:** | **1** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/66311105/badge)](https://coveralls.io/builds/66311105) |
| :-- | --: |
| Change from base [Build 8291420278](https://coveralls.io/builds/66307828): |  -0.09% |
| Covered Lines: | 3498 |
| Relevant Lines: | 3501 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
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


Test patch:
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/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) {