shithub: hugo

Download patch

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)
+			}
 		}
 	}
 }