Repository: jedib0t/go-pretty (go)

Issue:
Table.SortBy can't handle empty cells
**Describe the bug**

The last column of your test table has no values for two rows. Sorting that column fails. 

**To Reproduce**

Put this into table_test.go:

```go
func TestTable_MoreSort(t *testing.T) {
	table := Table{}
	table.AppendRows(testRows)
	table.AppendHeader(testHeader)
	table.SetStyle(styleTest)
	expectedOut := `(-----^------------^-----------^--------^-----------------------------)
[<#  >|<FIRST NAME>|<LAST NAME>|<SALARY>|<                           >]
{-----+------------+-----------+--------+-----------------------------}
[<20 >|<Jon       >|<Snow     >|<  2000>|<You know nothing, Jon Snow!>]
[<300>|<Tyrion    >|<Lannister>|<  5000>|<                           >]
[<1  >|<Arya      >|<Stark    >|<  3000>|<                           >]
\-----v------------v-----------v--------v-----------------------------/`
	table.SortBy([]SortBy{{Number: 5, Mode: Dsc}, {Name: "Last Name", Mode: Asc}})
	assert.Equal(t, expectedOut, table.Render())
}
```

**Expected behavior**

Expactation is that empty cells are sorted according to their string, representation.

The expected output is given in the code.

**Screenshots**

Actual output:

```text
(-----^------------^-----------^--------^-----------------------------)
[<  #>|<FIRST NAME>|<LAST NAME>|<SALARY>|<                           >]
{-----+------------+-----------+--------+-----------------------------}
[<300>|<Tyrion    >|<Lannister>|<  5000>|<                           >]
[< 20>|<Jon       >|<Snow     >|<  2000>|<You know nothing, Jon Snow!>]
[<  1>|<Arya      >|<Stark    >|<  3000>|<                           >]
\-----v------------v-----------v--------v-----------------------------/
```

As you see: Column 5 isn't properly sorted.

**Software (please complete the following information):**
 - OS: Mac OS - But I'm using a dockerized go
 - GoLang Version dockerized golang:latest



Pull request:
table: mixed-mode alignment and sorting
## Proposed Changes
  - table: Sort should now work on missing/empty cells; fixes #305
  - table: Sort supports `AlphaNumeric` modes
  - text: `AlignAuto` to align numbers Right and everything else Left


Patch:
diff --git a/table/sort.go b/table/sort.go
index ae55f30..ad229d6 100644
--- a/table/sort.go
+++ b/table/sort.go
@@ -25,12 +25,24 @@ type SortMode int
 const (
 	// Asc sorts the column in Ascending order alphabetically.
 	Asc SortMode = iota
+	// AscAlphaNumeric sorts the column in Ascending order alphabetically and
+	// then numerically.
+	AscAlphaNumeric
 	// AscNumeric sorts the column in Ascending order numerically.
 	AscNumeric
+	// AscNumericAlpha sorts the column in Ascending order numerically and
+	// then alphabetically.
+	AscNumericAlpha
 	// Dsc sorts the column in Descending order alphabetically.
 	Dsc
+	// DscAlphaNumeric sorts the column in Descending order alphabetically and
+	// then numerically.
+	DscAlphaNumeric
 	// DscNumeric sorts the column in Descending order numerically.
 	DscNumeric
+	// DscNumericAlpha sorts the column in Descending order numerically and
+	// then alphabetically.
+	DscNumericAlpha
 )
 
 type rowsSorter struct {
@@ -93,35 +105,105 @@ func (rs rowsSorter) Swap(i, j int) {
 
 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) {
-			shouldContinue, returnValue := rs.lessColumns(rowI, rowJ, colIdx, col)
-			if !shouldContinue {
-				return returnValue
-			}
+	for _, 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 := "", ""
+		if colIdx < len(rowI) {
+			iVal = rowI[colIdx]
+		}
+		if colIdx < len(rowJ) {
+			jVal = rowJ[colIdx]
+		}
+
+		// compare and choose whether to continue
+		shouldContinue, returnValue := less(iVal, jVal, sortBy.Mode)
+		if !shouldContinue {
+			return returnValue
 		}
 	}
 	return false
 }
 
-func (rs rowsSorter) lessColumns(rowI rowStr, rowJ rowStr, colIdx int, col SortBy) (bool, bool) {
-	if rowI[colIdx] == rowJ[colIdx] {
+func less(iVal string, jVal string, mode SortMode) (bool, bool) {
+	if iVal == jVal {
 		return true, false
-	} else if col.Mode == Asc {
-		return false, rowI[colIdx] < rowJ[colIdx]
-	} else if col.Mode == Dsc {
-		return false, rowI[colIdx] > rowJ[colIdx]
 	}
 
-	iVal, iErr := strconv.ParseFloat(rowI[colIdx], 64)
-	jVal, jErr := strconv.ParseFloat(rowJ[colIdx], 64)
-	if iErr == nil && jErr == nil {
-		if col.Mode == AscNumeric {
-			return false, iVal < jVal
-		} else if col.Mode == DscNumeric {
-			return false, jVal < iVal
-		}
+	switch mode {
+	case Asc, Dsc:
+		return lessAlphabetic(iVal, jVal, mode)
+	case AscNumeric, DscNumeric:
+		return lessNumeric(iVal, jVal, mode)
+	default: // AscAlphaNumeric, AscNumericAlpha, DscAlphaNumeric, DscNumericAlpha
+		return lessMixedMode(iVal, jVal, mode)
+	}
+}
+
+func lessAlphabetic(iVal string, jVal string, mode SortMode) (bool, bool) {
+	switch mode {
+	case Asc, AscAlphaNumeric, AscNumericAlpha:
+		return false, iVal < jVal
+	default: // Dsc, DscAlphaNumeric, DscNumericAlpha
+		return false, iVal > jVal
+	}
+}
+
+func lessAlphaNumericI(mode SortMode) (bool, bool) {
+	// i == "abc"; j == 5
+	switch mode {
+	case AscAlphaNumeric, DscAlphaNumeric:
+		return false, true
+	default: // AscNumericAlpha, DscNumericAlpha
+		return false, false
+	}
+}
+
+func lessAlphaNumericJ(mode SortMode) (bool, bool) {
+	// i == 5; j == "abc"
+	switch mode {
+	case AscAlphaNumeric, DscAlphaNumeric:
+		return false, false
+	default: // AscNumericAlpha, DscNumericAlpha:
+		return false, true
+	}
+}
+
+func lessMixedMode(iVal string, jVal string, mode SortMode) (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)
+	}
+	if iErr != nil { // iVal is alphabetic, jVal is numeric
+		return lessAlphaNumericI(mode)
+	}
+	if jErr != nil { // iVal is numeric, jVal is alphabetic
+		return lessAlphaNumericJ(mode)
+	}
+	// both values numeric
+	return lessNumericVal(iNumVal, jNumVal, mode)
+}
+
+func lessNumeric(iVal string, jVal string, mode SortMode) (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)
+}
+
+func lessNumericVal(iVal float64, jVal float64, mode SortMode) (bool, bool) {
+	if iVal == jVal {
+		return true, false
+	}
+
+	switch mode {
+	case AscNumeric, AscAlphaNumeric, AscNumericAlpha:
+		return false, iVal < jVal
+	default: // DscNumeric, DscAlphaNumeric, DscNumericAlpha
+		return false, iVal > jVal
 	}
-	return true, false
 }
diff --git a/text/align.go b/text/align.go
index 7a6d92c..c11b511 100644
--- a/text/align.go
+++ b/text/align.go
@@ -2,6 +2,7 @@ package text
 
 import (
 	"fmt"
+	"regexp"
 	"strconv"
 	"strings"
 	"unicode/utf8"
@@ -17,6 +18,12 @@ const (
 	AlignCenter               // "   center   "
 	AlignJustify              // "justify   it"
 	AlignRight                // "       right"
+	AlignAuto                 // AlignRight for numbers, AlignLeft for the rest
+)
+
+var (
+	// reNumericText - Regular Expression to match numbers.
+	reNumericText = regexp.MustCompile(`^\s*[+\-]?\d*[.]?\d+\s*$`)
 )
 
 // Apply aligns the text as directed. For ex.:
@@ -25,14 +32,25 @@ const (
 //   - AlignCenter.Apply("Jon Snow",  12) returns "  Jon Snow  "
 //   - AlignJustify.Apply("Jon Snow", 12) returns "Jon     Snow"
 //   - AlignRight.Apply("Jon Snow",   12) returns "    Jon Snow"
+//   - AlignAuto.Apply("Jon Snow",    12) returns "Jon Snow    "
 func (a Align) Apply(text string, maxLength int) string {
-	text = a.trimString(text)
+	aComputed := a
+	if aComputed == AlignAuto {
+		_, err := strconv.ParseFloat(text, 64)
+		if err == nil { // was able to parse a number out of the string
+			aComputed = AlignRight
+		} else {
+			aComputed = AlignLeft
+		}
+	}
+
+	text = aComputed.trimString(text)
 	sLen := utf8.RuneCountInString(text)
 	sLenWoE := RuneWidthWithoutEscSequences(text)
 	numEscChars := sLen - sLenWoE
 
 	// now, align the text
-	switch a {
+	switch aComputed {
 	case AlignDefault, AlignLeft:
 		return fmt.Sprintf("%-"+strconv.Itoa(maxLength+numEscChars)+"s", text)
 	case AlignCenter:
@@ -42,7 +60,7 @@ func (a Align) Apply(text string, maxLength int) string {
 				text+strings.Repeat(" ", (maxLength-sLenWoE)/2))
 		}
 	case AlignJustify:
-		return a.justifyText(text, sLenWoE, maxLength)
+		return justifyText(text, sLenWoE, maxLength)
 	}
 	return fmt.Sprintf("%"+strconv.Itoa(maxLength+numEscChars)+"s", text)
 }
@@ -77,16 +95,34 @@ func (a Align) MarkdownProperty() string {
 	}
 }
 
-func (a Align) justifyText(text string, textLength int, maxLength int) string {
+func (a Align) trimString(text string) string {
+	switch a {
+	case AlignDefault, AlignLeft:
+		if strings.HasSuffix(text, " ") {
+			return strings.TrimRight(text, " ")
+		}
+	case AlignRight:
+		if strings.HasPrefix(text, " ") {
+			return strings.TrimLeft(text, " ")
+		}
+	default:
+		if strings.HasPrefix(text, " ") || strings.HasSuffix(text, " ") {
+			return strings.Trim(text, " ")
+		}
+	}
+	return text
+}
+
+func justifyText(text string, textLength int, maxLength int) string {
 	// split the text into individual words
-	wordsUnfiltered := strings.Split(text, " ")
-	words := Filter(wordsUnfiltered, func(item string) bool {
+	words := Filter(strings.Split(text, " "), func(item string) bool {
 		return item != ""
 	})
-	// empty string implies spaces for maxLength
+	// empty string implies result is just spaces for maxLength
 	if len(words) == 0 {
 		return strings.Repeat(" ", maxLength)
 	}
+
 	// get the number of spaces to insert into the text
 	numSpacesNeeded := maxLength - textLength + strings.Count(text, " ")
 	numSpacesNeededBetweenWords := 0
@@ -117,21 +153,3 @@ func (a Align) justifyText(text string, textLength int, maxLength int) string {
 	}
 	return outText.String()
 }
-
-func (a Align) trimString(text string) string {
-	switch a {
-	case AlignDefault, AlignLeft:
-		if strings.HasSuffix(text, " ") {
-			return strings.TrimRight(text, " ")
-		}
-	case AlignRight:
-		if strings.HasPrefix(text, " ") {
-			return strings.TrimLeft(text, " ")
-		}
-	default:
-		if strings.HasPrefix(text, " ") || strings.HasSuffix(text, " ") {
-			return strings.Trim(text, " ")
-		}
-	}
-	return text
-}


Test patch:
diff --git a/table/render_test.go b/table/render_test.go
index d5969eb..4dc2e26 100644
--- a/table/render_test.go
+++ b/table/render_test.go
@@ -111,6 +111,34 @@ func TestTable_Render(t *testing.T) {
 A Song of Ice and Fire`)
 }
 
+func TestTable_Render_Align(t *testing.T) {
+	tw := NewWriter()
+	tw.AppendHeader(testHeader)
+	tw.AppendRows(testRows)
+	tw.AppendRow(Row{500, "Jamie", "Lannister", "Kingslayer", "The things I do for love."})
+	tw.AppendRow(Row{1000, "Tywin", "Lannister", nil})
+	tw.AppendFooter(testFooter)
+	tw.SetColumnConfigs([]ColumnConfig{
+		{Name: "First Name", Align: text.AlignLeft, AlignHeader: text.AlignLeft, AlignFooter: text.AlignLeft},
+		{Name: "Last Name", Align: text.AlignRight, AlignHeader: text.AlignRight, AlignFooter: text.AlignRight},
+		{Name: "Salary", Align: text.AlignAuto, AlignHeader: text.AlignRight, AlignFooter: text.AlignAuto},
+		{Number: 5, Align: text.AlignJustify, AlignHeader: text.AlignJustify, AlignFooter: text.AlignJustify},
+	})
+
+	compareOutput(t, tw.Render(), `
++------+------------+-----------+------------+-----------------------------+
+|    # | FIRST NAME | LAST NAME |     SALARY |                             |
++------+------------+-----------+------------+-----------------------------+
+|    1 | Arya       |     Stark |       3000 |                             |
+|   20 | Jon        |      Snow |       2000 | You know nothing, Jon Snow! |
+|  300 | Tyrion     | Lannister |       5000 |                             |
+|  500 | Jamie      | Lannister | Kingslayer | The things I do for   love. |
+| 1000 | Tywin      | Lannister | <nil>      |                             |
++------+------------+-----------+------------+-----------------------------+
+|      |            |     TOTAL |      10000 |                             |
++------+------------+-----------+------------+-----------------------------+`)
+}
+
 func TestTable_Render_AutoIndex(t *testing.T) {
 	tw := NewWriter()
 	for rowIdx := 0; rowIdx < 10; rowIdx++ {
diff --git a/table/sort_test.go b/table/sort_test.go
index d63164d..c6c05c5 100644
--- a/table/sort_test.go
+++ b/table/sort_test.go
@@ -6,6 +6,72 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+func TestTable_sortRows_MissingCells(t *testing.T) {
+	table := Table{}
+	table.AppendRows([]Row{
+		{1, "Arya", "Stark", 3000, 9},
+		{11, "Sansa", "Stark", 3000},
+		{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
+		{300, "Tyrion", "Lannister", 5000, 7},
+	})
+	table.SetStyle(StyleDefault)
+	table.initForRenderRows()
+
+	// sort by "First Name"
+	table.SortBy([]SortBy{{Number: 5, Mode: Asc}})
+	assert.Equal(t, []int{1, 3, 0, 2}, table.getSortedRowIndices())
+}
+
+func TestTable_sortRows_InvalidMode(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},
+	})
+	table.SetStyle(StyleDefault)
+	table.initForRenderRows()
+
+	// sort by "First Name"
+	table.SortBy([]SortBy{{Number: 2, Mode: AscNumeric}})
+	assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
+}
+
+func TestTable_sortRows_MixedMode(t *testing.T) {
+	table := Table{}
+	table.AppendHeader(Row{"#", "First Name", "Last Name", "Salary"})
+	table.AppendRows([]Row{
+		/* 0 */ {1, "Arya", "Stark", 3000, 4},
+		/* 1 */ {11, "Sansa", "Stark", 3000},
+		/* 2 */ {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
+		/* 3 */ {300, "Tyrion", "Lannister", 5000, -7.54},
+		/* 4 */ {400, "Jamie", "Lannister", 5000, nil},
+		/* 5 */ {500, "Tywin", "Lannister", 5000, "-7.540"},
+	})
+	table.SetStyle(StyleDefault)
+	table.initForRenderRows()
+
+	// sort by nothing
+	assert.Equal(t, []int{0, 1, 2, 3, 4, 5}, table.getSortedRowIndices())
+
+	// sort column #5 in Ascending order alphabetically and then numerically
+	table.SortBy([]SortBy{{Number: 5, Mode: AscAlphaNumeric}, {Number: 1, Mode: AscNumeric}})
+	assert.Equal(t, []int{1, 4, 2, 3, 5, 0}, table.getSortedRowIndices())
+
+	// sort column #5 in Ascending order numerically and then alphabetically
+	table.SortBy([]SortBy{{Number: 5, Mode: AscNumericAlpha}, {Number: 1, Mode: AscNumeric}})
+	assert.Equal(t, []int{3, 5, 0, 1, 4, 2}, table.getSortedRowIndices())
+
+	// sort column #5 in Descending order alphabetically and then numerically
+	table.SortBy([]SortBy{{Number: 5, Mode: DscAlphaNumeric}, {Number: 1, Mode: AscNumeric}})
+	assert.Equal(t, []int{2, 4, 1, 0, 3, 5}, table.getSortedRowIndices())
+
+	// sort column #5 in Descending order numerically and then alphabetically
+	table.SortBy([]SortBy{{Number: 5, Mode: DscNumericAlpha}, {Number: 1, Mode: AscNumeric}})
+	assert.Equal(t, []int{0, 3, 5, 2, 4, 1}, table.getSortedRowIndices())
+}
+
 func TestTable_sortRows_WithName(t *testing.T) {
 	table := Table{}
 	table.AppendHeader(Row{"#", "First Name", "Last Name", "Salary"})
@@ -130,19 +196,3 @@ func TestTable_sortRows_WithoutName(t *testing.T) {
 	table.SortBy(nil)
 	assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
 }
-
-func TestTable_sortRows_InvalidMode(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},
-	})
-	table.SetStyle(StyleDefault)
-	table.initForRenderRows()
-
-	// sort by "First Name"
-	table.SortBy([]SortBy{{Number: 2, Mode: AscNumeric}})
-	assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
-}
diff --git a/text/align_test.go b/text/align_test.go
index b3250d1..a148845 100644
--- a/text/align_test.go
+++ b/text/align_test.go
@@ -13,12 +13,16 @@ func ExampleAlign_Apply() {
 	fmt.Printf("AlignCenter : '%s'\n", AlignCenter.Apply("Jon Snow", 12))
 	fmt.Printf("AlignJustify: '%s'\n", AlignJustify.Apply("Jon Snow", 12))
 	fmt.Printf("AlignRight  : '%s'\n", AlignRight.Apply("Jon Snow", 12))
+	fmt.Printf("AlignAuto   : '%s'\n", AlignAuto.Apply("Jon Snow", 12))
+	fmt.Printf("AlignAuto   : '%s'\n", AlignAuto.Apply("-5.43", 12))
 
 	// Output: AlignDefault: 'Jon Snow    '
 	// AlignLeft   : 'Jon Snow    '
 	// AlignCenter : '  Jon Snow  '
 	// AlignJustify: 'Jon     Snow'
 	// AlignRight  : '    Jon Snow'
+	// AlignAuto   : 'Jon Snow    '
+	// AlignAuto   : '       -5.43'
 }
 
 func TestAlign_Apply(t *testing.T) {
@@ -50,6 +54,17 @@ func TestAlign_Apply(t *testing.T) {
 	assert.Equal(t, "   Jon Snow ", AlignRight.Apply("Jon Snow ", 12))
 	assert.Equal(t, "   Jon Snow ", AlignRight.Apply("  Jon Snow ", 12))
 	assert.Equal(t, "            ", AlignRight.Apply("", 12))
+
+	// Align Auto
+	assert.Equal(t, "Jon Snow    ", AlignAuto.Apply("Jon Snow", 12))
+	assert.Equal(t, "Jon Snow    ", AlignAuto.Apply("Jon Snow ", 12))
+	assert.Equal(t, "  Jon Snow  ", AlignAuto.Apply("  Jon Snow ", 12))
+	assert.Equal(t, "            ", AlignAuto.Apply("", 12))
+	assert.Equal(t, "          13", AlignAuto.Apply("13", 12))
+	assert.Equal(t, "       -5.43", AlignAuto.Apply("-5.43", 12))
+	assert.Equal(t, "        +.43", AlignAuto.Apply("+.43", 12))
+	assert.Equal(t, "       +5.43", AlignAuto.Apply("+5.43", 12))
+	assert.Equal(t, "+5.43x      ", AlignAuto.Apply("+5.43x", 12))
 }
 
 func TestAlign_Apply_ColoredText(t *testing.T) {