diff --git a/table/sort.go b/table/sort.go index 8110806..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 { @@ -94,8 +106,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 { - rowI, rowJ, colIdx := rs.rows[realI], rs.rows[realJ], sortBy.Number-1 // 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] @@ -103,8 +115,9 @@ func (rs rowsSorter) Less(i, j int) bool { if colIdx < len(rowJ) { jVal = rowJ[colIdx] } + // compare and choose whether to continue - shouldContinue, returnValue := rs.lessColumns(iVal, jVal, sortBy) + shouldContinue, returnValue := less(iVal, jVal, sortBy.Mode) if !shouldContinue { return returnValue } @@ -112,23 +125,85 @@ func (rs rowsSorter) Less(i, j int) bool { return false } -func (rs rowsSorter) lessColumns(iVal string, jVal string, sortBy SortBy) (bool, bool) { +func less(iVal string, jVal string, mode SortMode) (bool, bool) { if iVal == jVal { return true, false - } else if sortBy.Mode == Asc { + } + + 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 - } else if sortBy.Mode == Dsc { + 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 { - if sortBy.Mode == AscNumeric { - return false, iNumVal < jNumVal - } else if sortBy.Mode == DscNumeric { - return false, jNumVal < iNumVal - } + 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/table/sort_test.go b/table/sort_test.go index d4cb5fc..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,35 +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_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()) -}