Skip to content

Commit 99d6ff2

Browse files
committed
Implement sort-by-column feature
The `Table` interface gains `SortByNamedColumn()` and `SortByColumnNumber()` which sort the table rows in-place. err = tb.SortByNamedColumn("Some Header", tabular.SORT_ASC) Introduce the `SortInter` interface which a value stored in a Cell can implement; a method `SortInt64() int64` will let objects define their sort order. See the `sort_test.go` sorting by position-in-greek-alphabet for an example. Fixed nested `*Cell` inside `Cell` to propagate rendering values. Fixed that table `columnNames` field was never populated. We populate it afresh on every `AddHeader` call.
1 parent a5a8ded commit 99d6ff2

File tree

6 files changed

+444
-5
lines changed

6 files changed

+444
-5
lines changed

atable.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright © 2016,2018 Pennock Tech, LLC.
1+
// Copyright © 2016,2018,2025 Pennock Tech, LLC.
22
// All rights reserved, except as granted under license.
33
// Licensed per file LICENSE.txt
44

@@ -143,11 +143,15 @@ func (t *ATable) Headers() []Cell {
143143
func (t *ATable) AddHeaders(items ...interface{}) Table {
144144
t.resizeColumnsAtLeast(len(items))
145145
hr := NewRowWithCapacity(len(items))
146+
columnNames := make(map[string]int, len(items))
146147
hr.ErrorContainer = t.ErrorContainer
147148
for i := range items {
148-
hr.Add(NewCell(items[i]))
149+
cell := NewCell(items[i])
150+
hr.Add(cell)
151+
columnNames[cell.String()] = i
149152
}
150153
t.headerRow = hr
154+
t.columnNames = columnNames
151155

152156
invokePropertyCallbacks(t.tableRowAdditionCallbacks, CB_AT_ADD, hr, t.ErrorContainer)
153157
for i := range hr.cells {

cell.go

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
// Copyright © 2016,2018 Pennock Tech, LLC.
1+
// Copyright © 2016,2018,2025 Pennock Tech, LLC.
22
// All rights reserved, except as granted under license.
33
// Licensed per file LICENSE.txt
44

55
package tabular // import "go.pennock.tech/tabular"
66

77
import (
88
"fmt"
9+
"reflect"
910
"strings"
1011

1112
"go.pennock.tech/tabular/length"
@@ -69,6 +70,12 @@ func (c *Cell) Update() {
6970
c.height = o.height
7071
c.empty = o.empty
7172
return
73+
case *Cell:
74+
c.str = o.str
75+
c.width = o.width
76+
c.height = o.height
77+
c.empty = o.empty
78+
return
7279

7380
// After this point, MUST set .str
7481
case string:
@@ -189,3 +196,101 @@ func (c *Cell) Empty() bool {
189196
}
190197
return c.empty
191198
}
199+
200+
// LessThan returns true if the value of this cell is less than the value of
201+
// the other cell. The determination of "less" is euphemistically heuristic.
202+
func (c *Cell) LessThan(d *Cell) bool {
203+
var (
204+
g, h, t *Cell
205+
u Cell
206+
ok bool
207+
cv, dv reflect.Value
208+
tt, sortIntType reflect.Type
209+
as string
210+
af float64
211+
aOkay bool
212+
)
213+
214+
g, ok = c, true
215+
for ok {
216+
if t, ok = g.raw.(*Cell); ok {
217+
g = t
218+
} else if u, ok = g.raw.(Cell); ok {
219+
g = &u
220+
}
221+
}
222+
cv = reflect.ValueOf(g.raw)
223+
224+
h, ok = d, true
225+
for ok {
226+
if t, ok = h.raw.(*Cell); ok {
227+
h = t
228+
} else if u, ok = h.raw.(Cell); ok {
229+
h = &u
230+
}
231+
}
232+
dv = reflect.ValueOf(h.raw)
233+
234+
// Do not try to convert to uint, because positive floats convert and lose precision.
235+
// Similarly for int.
236+
// Leave _conversions_ for the float. But "can" is the underlying type.
237+
// We want to use SortInter as our _first_ choice, including when defined on types for which the underlying type is an int
238+
239+
sortIntType = reflect.TypeOf((*SortInter)(nil)).Elem()
240+
241+
if cv.Type().Implements(sortIntType) {
242+
if dv.Type().Implements(sortIntType) {
243+
return cv.Interface().(SortInter).SortInt64() < dv.Interface().(SortInter).SortInt64()
244+
} else if dv.CanInt() {
245+
return cv.Interface().(SortInter).SortInt64() < dv.Int()
246+
}
247+
} else if dv.Type().Implements(sortIntType) {
248+
if cv.CanInt() {
249+
return cv.Int() < dv.Interface().(SortInter).SortInt64()
250+
}
251+
}
252+
253+
if cv.CanFloat() && dv.CanFloat() {
254+
return cv.Float() < dv.Float()
255+
}
256+
if cv.CanUint() && dv.CanUint() {
257+
return cv.Uint() < dv.Uint()
258+
}
259+
if cv.CanInt() {
260+
if dv.CanFloat() {
261+
return float64(cv.Int()) < dv.Float()
262+
} else if dv.CanInt() {
263+
return cv.Int() < dv.Int()
264+
}
265+
} else if cv.CanFloat() && dv.CanInt() {
266+
return cv.Float() < float64(dv.Int())
267+
}
268+
269+
tt = reflect.TypeOf(af)
270+
if cv.CanConvert(tt) && dv.CanConvert(tt) {
271+
return cv.Convert(tt).Float() < dv.Convert(tt).Float()
272+
}
273+
274+
aOkay = false
275+
if x, ok := g.raw.(string); ok {
276+
as = x
277+
aOkay = true
278+
} else if x, ok := g.raw.(Stringer); ok {
279+
as = x.String()
280+
aOkay = true
281+
} else if x, ok := g.raw.(GoStringer); ok {
282+
as = x.GoString()
283+
aOkay = true
284+
}
285+
if aOkay {
286+
if x, ok := h.raw.(string); ok {
287+
return as < x
288+
} else if x, ok := h.raw.(Stringer); ok {
289+
return as < x.String()
290+
} else if x, ok := h.raw.(GoStringer); ok {
291+
return as < x.GoString()
292+
}
293+
}
294+
295+
return false
296+
}

sort.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright © 2025 Pennock Tech, LLC.
2+
// All rights reserved, except as granted under license.
3+
// Licensed per file LICENSE.txt
4+
5+
package tabular // import "go.pennock.tech/tabular"
6+
7+
import (
8+
"errors"
9+
"sort"
10+
"strconv"
11+
)
12+
13+
type ErrorNoSuchColumn string
14+
15+
func (e ErrorNoSuchColumn) Error() string { return "no such column " + strconv.Quote(string(e)) }
16+
17+
type ErrorColumnOutOfRange int
18+
19+
func (e ErrorColumnOutOfRange) Error() string {
20+
return "column " + strconv.Itoa(int(e)) + " out of range"
21+
}
22+
23+
var ErrNoColumnHeaders = errors.New("no headers have defined columns")
24+
25+
type SortOrder int
26+
27+
const (
28+
SORT_ASC SortOrder = iota + 1
29+
SORT_DESC
30+
)
31+
32+
func (so SortOrder) String() string {
33+
switch so {
34+
case SORT_ASC:
35+
return "ascending"
36+
case SORT_DESC:
37+
return "descending"
38+
default:
39+
panic("unhandled sort order for String")
40+
}
41+
}
42+
43+
// SortByNamedColumn performs an in-place row-sort.
44+
func (t *ATable) SortByNamedColumn(name string, order SortOrder) error {
45+
if t.columnNames == nil {
46+
return ErrNoColumnHeaders
47+
}
48+
columnNumber, ok := t.columnNames[name]
49+
if !ok {
50+
return ErrorNoSuchColumn(name)
51+
}
52+
return t.SortByColumnNumber(columnNumber, order)
53+
}
54+
55+
type tableSorter struct {
56+
tb *ATable
57+
column int
58+
order SortOrder
59+
}
60+
61+
func (t tableSorter) Len() int { return t.tb.NRows() }
62+
func (t tableSorter) Swap(i, j int) {
63+
t.tb.rows[i], t.tb.rows[j] = t.tb.rows[j], t.tb.rows[i]
64+
}
65+
func (t tableSorter) Less(i, j int) bool {
66+
if t.order == SORT_ASC {
67+
return t.tb.rows[i].cells[t.column].LessThan(&t.tb.rows[j].cells[t.column])
68+
} else if t.order == SORT_DESC {
69+
return t.tb.rows[j].cells[t.column].LessThan(&t.tb.rows[i].cells[t.column])
70+
} else {
71+
panic("unhandled order in tableSorter.Less")
72+
}
73+
}
74+
75+
func (t *ATable) SortByColumnNumber(sortCol int, order SortOrder) error {
76+
if sortCol < 0 || sortCol >= t.nColumns {
77+
return ErrorColumnOutOfRange(sortCol)
78+
}
79+
ts := tableSorter{t, sortCol, order}
80+
sort.Sort(ts)
81+
return nil
82+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy