mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
Make sort tpl func accept field/key chaining arg
'sort' template function used to accept only each element's struct field name, method name and map key name as its second argument. This extends it to accept a field/method/key chaining key string like 'Params.foo.bar' as the argument. It evaluates sub elements of each array or map elements and sorts by them. Typical use case would be sorting pages by user defined front matter value. For example, sorting pages by 'Params.foo.bar' is possible by writing the following template code {{ range sort .Data.Pages "Params.foo.bar" }} {{ .Content }} {{ end }} It ignores all leading and trailing dots so "Params.foo.bar" can be written in ".Params.foo.bar" This also fixes the issue that 'sort' cannot evaluate a pointer value. Fix #1330
This commit is contained in:
parent
56534beaf6
commit
153332706a
2 changed files with 181 additions and 40 deletions
|
@ -882,32 +882,51 @@ func Sort(seq interface{}, args ...interface{}) (interface{}, error) {
|
|||
return nil, errors.New("can't iterate over a nil value")
|
||||
}
|
||||
|
||||
switch seqv.Kind() {
|
||||
case reflect.Array, reflect.Slice, reflect.Map:
|
||||
// ok
|
||||
default:
|
||||
return nil, errors.New("can't sort " + reflect.ValueOf(seq).Type().String())
|
||||
}
|
||||
|
||||
// Create a list of pairs that will be used to do the sort
|
||||
p := pairList{SortAsc: true, SliceType: reflect.SliceOf(seqv.Type().Elem())}
|
||||
p.Pairs = make([]pair, seqv.Len())
|
||||
|
||||
var sortByField string
|
||||
for i, l := range args {
|
||||
dStr, err := cast.ToStringE(l)
|
||||
switch {
|
||||
case i == 0 && err != nil:
|
||||
p.SortByField = ""
|
||||
sortByField = ""
|
||||
case i == 0 && err == nil:
|
||||
p.SortByField = dStr
|
||||
sortByField = dStr
|
||||
case i == 1 && err == nil && dStr == "desc":
|
||||
p.SortAsc = false
|
||||
case i == 1:
|
||||
p.SortAsc = true
|
||||
}
|
||||
}
|
||||
path := strings.Split(strings.Trim(sortByField, "."), ".")
|
||||
|
||||
switch seqv.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
for i := 0; i < seqv.Len(); i++ {
|
||||
p.Pairs[i].Key = reflect.ValueOf(i)
|
||||
p.Pairs[i].Value = seqv.Index(i)
|
||||
}
|
||||
if p.SortByField == "" {
|
||||
p.SortByField = "value"
|
||||
if sortByField == "" || sortByField == "value" {
|
||||
p.Pairs[i].SortByValue = p.Pairs[i].Value
|
||||
} else {
|
||||
v := p.Pairs[i].Value
|
||||
var err error
|
||||
for _, elemName := range path {
|
||||
v, err = evaluateSubElem(v, elemName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
p.Pairs[i].SortByValue = v
|
||||
}
|
||||
}
|
||||
|
||||
case reflect.Map:
|
||||
|
@ -915,10 +934,22 @@ func Sort(seq interface{}, args ...interface{}) (interface{}, error) {
|
|||
for i := 0; i < seqv.Len(); i++ {
|
||||
p.Pairs[i].Key = keys[i]
|
||||
p.Pairs[i].Value = seqv.MapIndex(keys[i])
|
||||
if sortByField == "" {
|
||||
p.Pairs[i].SortByValue = p.Pairs[i].Key
|
||||
} else if sortByField == "value" {
|
||||
p.Pairs[i].SortByValue = p.Pairs[i].Value
|
||||
} else {
|
||||
v := p.Pairs[i].Value
|
||||
var err error
|
||||
for _, elemName := range path {
|
||||
v, err = evaluateSubElem(v, elemName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
p.Pairs[i].SortByValue = v
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, errors.New("can't sort " + reflect.ValueOf(seq).Type().String())
|
||||
}
|
||||
return p.sort(), nil
|
||||
}
|
||||
|
@ -927,40 +958,22 @@ func Sort(seq interface{}, args ...interface{}) (interface{}, error) {
|
|||
// https://groups.google.com/forum/#!topic/golang-nuts/FT7cjmcL7gw
|
||||
// A data structure to hold a key/value pair.
|
||||
type pair struct {
|
||||
Key reflect.Value
|
||||
Value reflect.Value
|
||||
Key reflect.Value
|
||||
Value reflect.Value
|
||||
SortByValue reflect.Value
|
||||
}
|
||||
|
||||
// A slice of pairs that implements sort.Interface to sort by Value.
|
||||
type pairList struct {
|
||||
Pairs []pair
|
||||
SortByField string
|
||||
SortAsc bool
|
||||
SliceType reflect.Type
|
||||
Pairs []pair
|
||||
SortAsc bool
|
||||
SliceType reflect.Type
|
||||
}
|
||||
|
||||
func (p pairList) Swap(i, j int) { p.Pairs[i], p.Pairs[j] = p.Pairs[j], p.Pairs[i] }
|
||||
func (p pairList) Len() int { return len(p.Pairs) }
|
||||
func (p pairList) Less(i, j int) bool {
|
||||
var truth bool
|
||||
switch {
|
||||
case p.SortByField == "value":
|
||||
iVal := p.Pairs[i].Value
|
||||
jVal := p.Pairs[j].Value
|
||||
truth = Lt(iVal.Interface(), jVal.Interface())
|
||||
|
||||
case p.SortByField != "":
|
||||
if p.Pairs[i].Value.FieldByName(p.SortByField).IsValid() {
|
||||
iVal := p.Pairs[i].Value.FieldByName(p.SortByField)
|
||||
jVal := p.Pairs[j].Value.FieldByName(p.SortByField)
|
||||
truth = Lt(iVal.Interface(), jVal.Interface())
|
||||
}
|
||||
default:
|
||||
iVal := p.Pairs[i].Key
|
||||
jVal := p.Pairs[j].Key
|
||||
truth = Lt(iVal.Interface(), jVal.Interface())
|
||||
}
|
||||
return truth
|
||||
return Lt(p.Pairs[i].SortByValue.Interface(), p.Pairs[j].SortByValue.Interface())
|
||||
}
|
||||
|
||||
// sorts a pairList and returns a slice of sorted values
|
||||
|
|
|
@ -1081,6 +1081,10 @@ func TestSort(t *testing.T) {
|
|||
MyFloat float64
|
||||
MyString string
|
||||
}
|
||||
type mid struct {
|
||||
Tst TstX
|
||||
}
|
||||
|
||||
for i, this := range []struct {
|
||||
sequence interface{}
|
||||
sortByField interface{}
|
||||
|
@ -1091,6 +1095,8 @@ func TestSort(t *testing.T) {
|
|||
{[]string{"class3", "class1", "class2"}, nil, "asc", []string{"class1", "class2", "class3"}},
|
||||
{[]int{1, 2, 3, 4, 5}, nil, "asc", []int{1, 2, 3, 4, 5}},
|
||||
{[]int{5, 4, 3, 1, 2}, nil, "asc", []int{1, 2, 3, 4, 5}},
|
||||
// test sort key parameter is focibly set empty
|
||||
{[]string{"class3", "class1", "class2"}, map[int]string{1: "a"}, "asc", []string{"class1", "class2", "class3"}},
|
||||
// test map sorting by keys
|
||||
{map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, nil, "asc", []int{10, 20, 30, 40, 50}},
|
||||
{map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, nil, "asc", []int{30, 20, 10, 40, 50}},
|
||||
|
@ -1122,9 +1128,124 @@ func TestSort(t *testing.T) {
|
|||
"asc",
|
||||
[]ts{{50, 50.5, "fifty"}, {40, 40.5, "forty"}, {10, 10.5, "ten"}, {30, 30.5, "thirty"}, {20, 20.5, "twenty"}},
|
||||
},
|
||||
// Test sort desc
|
||||
// test sort desc
|
||||
{[]string{"class1", "class2", "class3"}, "value", "desc", []string{"class3", "class2", "class1"}},
|
||||
{[]string{"class3", "class1", "class2"}, "value", "desc", []string{"class3", "class2", "class1"}},
|
||||
// test sort by struct's method
|
||||
{
|
||||
[]TstX{{A: "i", B: "j"}, {A: "e", B: "f"}, {A: "c", B: "d"}, {A: "g", B: "h"}, {A: "a", B: "b"}},
|
||||
"TstRv",
|
||||
"asc",
|
||||
[]TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}},
|
||||
},
|
||||
{
|
||||
[]*TstX{{A: "i", B: "j"}, {A: "e", B: "f"}, {A: "c", B: "d"}, {A: "g", B: "h"}, {A: "a", B: "b"}},
|
||||
"TstRp",
|
||||
"asc",
|
||||
[]*TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}},
|
||||
},
|
||||
// test map sorting by struct's method
|
||||
{
|
||||
map[string]TstX{"1": {A: "i", B: "j"}, "2": {A: "e", B: "f"}, "3": {A: "c", B: "d"}, "4": {A: "g", B: "h"}, "5": {A: "a", B: "b"}},
|
||||
"TstRv",
|
||||
"asc",
|
||||
[]TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}},
|
||||
},
|
||||
{
|
||||
map[string]*TstX{"1": {A: "i", B: "j"}, "2": {A: "e", B: "f"}, "3": {A: "c", B: "d"}, "4": {A: "g", B: "h"}, "5": {A: "a", B: "b"}},
|
||||
"TstRp",
|
||||
"asc",
|
||||
[]*TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}},
|
||||
},
|
||||
// test sort by dot chaining key argument
|
||||
{
|
||||
[]map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}},
|
||||
"foo.A",
|
||||
"asc",
|
||||
[]map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}},
|
||||
},
|
||||
{
|
||||
[]map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}},
|
||||
".foo.A",
|
||||
"asc",
|
||||
[]map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}},
|
||||
},
|
||||
{
|
||||
[]map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}},
|
||||
"foo.TstRv",
|
||||
"asc",
|
||||
[]map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}},
|
||||
},
|
||||
{
|
||||
[]map[string]*TstX{{"foo": &TstX{A: "e", B: "f"}}, {"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}},
|
||||
"foo.TstRp",
|
||||
"asc",
|
||||
[]map[string]*TstX{{"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}},
|
||||
},
|
||||
{
|
||||
[]map[string]mid{{"foo": mid{Tst: TstX{A: "e", B: "f"}}}, {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}},
|
||||
"foo.Tst.A",
|
||||
"asc",
|
||||
[]map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}},
|
||||
},
|
||||
{
|
||||
[]map[string]mid{{"foo": mid{Tst: TstX{A: "e", B: "f"}}}, {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}},
|
||||
"foo.Tst.TstRv",
|
||||
"asc",
|
||||
[]map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}},
|
||||
},
|
||||
// test map sorting by dot chaining key argument
|
||||
{
|
||||
map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}},
|
||||
"foo.A",
|
||||
"asc",
|
||||
[]map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}},
|
||||
},
|
||||
{
|
||||
map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}},
|
||||
".foo.A",
|
||||
"asc",
|
||||
[]map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}},
|
||||
},
|
||||
{
|
||||
map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}},
|
||||
"foo.TstRv",
|
||||
"asc",
|
||||
[]map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}},
|
||||
},
|
||||
{
|
||||
map[string]map[string]*TstX{"1": {"foo": &TstX{A: "e", B: "f"}}, "2": {"foo": &TstX{A: "a", B: "b"}}, "3": {"foo": &TstX{A: "c", B: "d"}}},
|
||||
"foo.TstRp",
|
||||
"asc",
|
||||
[]map[string]*TstX{{"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}},
|
||||
},
|
||||
{
|
||||
map[string]map[string]mid{"1": {"foo": mid{Tst: TstX{A: "e", B: "f"}}}, "2": {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, "3": {"foo": mid{Tst: TstX{A: "c", B: "d"}}}},
|
||||
"foo.Tst.A",
|
||||
"asc",
|
||||
[]map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}},
|
||||
},
|
||||
{
|
||||
map[string]map[string]mid{"1": {"foo": mid{Tst: TstX{A: "e", B: "f"}}}, "2": {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, "3": {"foo": mid{Tst: TstX{A: "c", B: "d"}}}},
|
||||
"foo.Tst.TstRv",
|
||||
"asc",
|
||||
[]map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}},
|
||||
},
|
||||
// test error cases
|
||||
{(*[]TstX)(nil), nil, "asc", false},
|
||||
{TstX{A: "a", B: "b"}, nil, "asc", false},
|
||||
{
|
||||
[]map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}},
|
||||
"foo.NotAvailable",
|
||||
"asc",
|
||||
false,
|
||||
},
|
||||
{
|
||||
map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}},
|
||||
"foo.NotAvailable",
|
||||
"asc",
|
||||
false,
|
||||
},
|
||||
} {
|
||||
var result interface{}
|
||||
var err error
|
||||
|
@ -1133,12 +1254,19 @@ func TestSort(t *testing.T) {
|
|||
} else {
|
||||
result, err = Sort(this.sequence, this.sortByField, this.sortAsc)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("[%d] failed: %s", i, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(result, this.expect) {
|
||||
t.Errorf("[%d] Sort called on sequence: %v | sortByField: `%v` | got %v but expected %v", i, this.sequence, this.sortByField, result, this.expect)
|
||||
|
||||
if b, ok := this.expect.(bool); ok && !b {
|
||||
if err == nil {
|
||||
t.Errorf("[%d] Sort didn't return an expected error", i)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("[%d] failed: %s", i, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(result, this.expect) {
|
||||
t.Errorf("[%d] Sort called on sequence: %v | sortByField: `%v` | got %v but expected %v", i, this.sequence, this.sortByField, result, this.expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue