add -k<name> and sort by multiple columns support, fixes #34

This commit is contained in:
2025-01-15 18:51:15 +01:00
committed by T.v.Dein
parent c2e7d8037a
commit 63c7ef26b6
13 changed files with 131 additions and 57 deletions

View File

@@ -99,6 +99,19 @@ func PrepareTransposerColumns(conf *cfg.Config, data *Tabdata) error {
return nil
}
// output option, prepare -k1,2 sort fields
func PrepareSortColumns(conf *cfg.Config, data *Tabdata) error {
// -c columns
usecolumns, err := PrepareColumnVars(conf.SortByColumn, data)
if err != nil {
return err
}
conf.UseSortByColumn = usecolumns
return nil
}
func PrepareColumnVars(columns string, data *Tabdata) ([]int, error) {
if columns == "" {
return nil, nil

View File

@@ -48,6 +48,11 @@ func ProcessFiles(conf *cfg.Config, args []string) error {
return err
}
err = PrepareSortColumns(conf, &data)
if err != nil {
return err
}
err = PrepareColumns(conf, &data)
if err != nil {
return err

View File

@@ -33,15 +33,17 @@ import (
)
func printData(writer io.Writer, conf cfg.Config, data *Tabdata) {
// add numbers to headers and remove this we're not interested in
// Sort the data first, before headers+entries are being
// reduced. That way the user can specify any valid column to sort
// by, independently if it's being used for display or not.
sortTable(conf, data)
// add numbers to headers and remove those we're not interested in
numberizeAndReduceHeaders(conf, data)
// remove unwanted columns, if any
reduceColumns(conf, data)
// sort the data
sortTable(conf, data)
switch conf.OutputMode {
case cfg.Extended:
printExtendedData(writer, conf, data)

View File

@@ -63,7 +63,7 @@ var tests = []struct {
name string // so we can identify which one fails, can be the same
// for multiple tests, because flags will be appended to the name
sortby string // empty == default
column int // sort by this column, 0 == default first or NO Sort
column int // sort by this column (numbers start by 1)
desc bool // sort in descending order, default == ascending
nonum bool // hide numbering
mode int // shell, orgtbl, etc. empty == default: ascii
@@ -162,7 +162,7 @@ DURATION(2): 33d12h
//------------------------ SORT TESTS
{
name: "sortbycolumn",
name: "sortbycolumn3",
column: 3,
sortby: "numeric",
desc: false,
@@ -173,7 +173,7 @@ beta 1d10h5m1s 33 3/1/2014
alpha 4h35m 170 2013-Feb-03`,
},
{
name: "sortbycolumn",
name: "sortbycolumn4",
column: 4,
sortby: "time",
desc: false,
@@ -184,7 +184,7 @@ alpha 4h35m 170 2013-Feb-03
beta 1d10h5m1s 33 3/1/2014`,
},
{
name: "sortbycolumn",
name: "sortbycolumn2",
column: 2,
sortby: "duration",
desc: false,
@@ -251,15 +251,14 @@ DURATION(2) WHEN(4)
func TestPrinter(t *testing.T) {
for _, testdata := range tests {
testname := fmt.Sprintf("print-sortcol-%d-desc-%t-sortby-%s-mode-%d-usecolumns-%s",
testdata.column, testdata.desc, testdata.sortby, testdata.mode, testdata.usecolstr)
testname := fmt.Sprintf("print-%s-%d-desc-%t-sortby-%s-mode-%d-usecolumns-%s",
testdata.name, testdata.column, testdata.desc, testdata.sortby, testdata.mode, testdata.usecolstr)
t.Run(testname, func(t *testing.T) {
// replaces os.Stdout, but we ignore it
var writer bytes.Buffer
// cmd flags
conf := cfg.Config{
SortByColumn: testdata.column,
SortDescending: testdata.desc,
SortMode: testdata.sortby,
OutputMode: testdata.mode,
@@ -268,6 +267,10 @@ func TestPrinter(t *testing.T) {
NoColor: true,
}
if testdata.column > 0 {
conf.UseSortByColumn = []int{testdata.column}
}
conf.ApplyDefaults()
// the test checks the len!

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package lib
import (
"cmp"
"regexp"
"sort"
"strconv"
@@ -27,34 +28,41 @@ import (
)
func sortTable(conf cfg.Config, data *Tabdata) {
if conf.SortByColumn <= 0 {
if len(conf.UseSortByColumn) == 0 {
// no sorting wanted
return
}
// slightly modified here to match internal array indicies
col := conf.SortByColumn
col-- // ui starts counting by 1, but use 0 internally
// sanity checks
if len(data.entries) == 0 {
return
}
if col >= len(data.headers) {
// fall back to default column
col = 0
}
// actual sorting
sort.SliceStable(data.entries, func(i, j int) bool {
return compare(&conf, data.entries[i][col], data.entries[j][col])
// holds the result of a sort of one column
comparators := []int{}
// iterate over all columns to be sorted, conf.SortMode must be identical!
for _, column := range conf.UseSortByColumn {
comparators = append(comparators, compare(&conf, data.entries[i][column-1], data.entries[j][column-1]))
}
// return the combined result
res := cmp.Or(comparators...)
switch res {
case 0:
return true
default:
return false
}
})
}
// config is not modified here, but it would be inefficient to copy it every loop
func compare(conf *cfg.Config, left string, right string) bool {
func compare(conf *cfg.Config, left string, right string) int {
var comp bool
switch conf.SortMode {
@@ -88,7 +96,12 @@ func compare(conf *cfg.Config, left string, right string) bool {
comp = !comp
}
return comp
switch comp {
case true:
return 0
default:
return 1
}
}
/*

View File

@@ -53,18 +53,18 @@ func TestCompare(t *testing.T) {
mode string
a string
b string
want bool
want int
desc bool
}{
// ascending
{"numeric", "10", "20", true, false},
{"duration", "2d4h5m", "45m", false, false},
{"time", "12/24/2022", "1/1/1970", false, false},
{"numeric", "10", "20", 0, false},
{"duration", "2d4h5m", "45m", 1, false},
{"time", "12/24/2022", "1/1/1970", 1, false},
// descending
{"numeric", "10", "20", false, true},
{"duration", "2d4h5m", "45m", true, true},
{"time", "12/24/2022", "1/1/1970", true, true},
{"numeric", "10", "20", 1, true},
{"duration", "2d4h5m", "45m", 0, true},
{"time", "12/24/2022", "1/1/1970", 0, true},
}
for _, testdata := range tests {
@@ -75,7 +75,7 @@ func TestCompare(t *testing.T) {
c := cfg.Config{SortMode: testdata.mode, SortDescending: testdata.desc}
got := compare(&c, testdata.a, testdata.b)
if got != testdata.want {
t.Errorf("got %t, want %t", got, testdata.want)
t.Errorf("got %d, want %d", got, testdata.want)
}
})
}