ref: 153332706a78d387225bc1d9e04bc8270cf49aff
parent: 56534beaf684f374327b6d01be26d15ae41c8985
author: Tatsushi Demachi <[email protected]>
date: Fri Aug 7 16:05:18 EDT 2015
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
--- a/tpl/template_funcs.go
+++ b/tpl/template_funcs.go
@@ -882,17 +882,25 @@
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:
@@ -899,6 +907,7 @@
p.SortAsc = true
}
}
+ path := strings.Split(strings.Trim(sortByField, "."), ".")
switch seqv.Kind() {
case reflect.Array, reflect.Slice:
@@ -905,10 +914,20 @@
for i := 0; i < seqv.Len(); i++ {
p.Pairs[i].Key = reflect.ValueOf(i)
p.Pairs[i].Value = seqv.Index(i)
+ 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
+ }
}
- if p.SortByField == "" {
- p.SortByField = "value"
- }
case reflect.Map:
keys := seqv.MapKeys()
@@ -915,10 +934,22 @@
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 @@
// 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
--- a/tpl/template_funcs_test.go
+++ b/tpl/template_funcs_test.go
@@ -1081,6 +1081,10 @@
MyFloat float64
MyString string
}
+ type mid struct {
+ Tst TstX
+ }
+
for i, this := range []struct {
sequence interface{}
sortByField interface{}
@@ -1091,6 +1095,8 @@
{[]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 @@
"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 @@
} 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)
+ }
}
}
}