shithub: hugo

Download patch

ref: 5d0748ce51d0a86843bfd6569fd3cd18fa20ed5a
parent: 63e2a46f630ea95b6a82ddd7af240f87026871fb
author: digitalcraftsman <[email protected]>
date: Sun Mar 12 19:04:12 EDT 2017

tpl: Add union template func


--- a/docs/content/templates/functions.md
+++ b/docs/content/templates/functions.md
@@ -214,6 +214,23 @@
     </ul>
 
 
+### union
+Given two arrays (or slices) A and B, this function will return a new array that contains the elements or objects that belong to either A or to B or to both. The elements supported are strings, integers and floats (only float64).
+
+```
+{{ union (slice 1 2 3) (slice 3 4 5) }}
+<!-- returns [1 2 3 4 5] -->
+
+{{ union (slice 1 2 3) nil }}
+<!-- returns [1 2 3] -->
+
+{{ union nil (slice 1 2 3) }}
+<!-- returns [1 2 3] -->
+
+{{ union nil nil }}
+<!-- returns an error because both arrays/slices have to be of the same type -->
+```
+
 ### isset
 Returns true if the parameter is set.
 Takes either a slice, array or channel and an index or a map and a key as input.
--- a/tpl/tplimpl/template_funcs.go
+++ b/tpl/tplimpl/template_funcs.go
@@ -399,6 +399,55 @@
 	}
 }
 
+// union returns the union of the given sets, l1 and l2. l1 and
+// l2 must be of the same type and may be either arrays or slices.
+// If l1 and l2 aren't of the same type then l1 will be returned.
+// If either l1 or l2 is nil then the non-nil list will be returned.
+func union(l1, l2 interface{}) (interface{}, error) {
+	if l1 == nil && l2 == nil {
+		return nil, errors.New("both arrays/slices have to be of the same type")
+	} else if l1 == nil && l2 != nil {
+		return l2, nil
+	} else if l1 != nil && l2 == nil {
+		return l1, nil
+	}
+
+	l1v := reflect.ValueOf(l1)
+	l2v := reflect.ValueOf(l2)
+
+	switch l1v.Kind() {
+	case reflect.Array, reflect.Slice:
+		switch l2v.Kind() {
+		case reflect.Array, reflect.Slice:
+			r := reflect.MakeSlice(l1v.Type(), 0, 0)
+
+			if l1v.Type() != l2v.Type() {
+				return r.Interface(), nil
+			}
+
+			for i := 0; i < l1v.Len(); i++ {
+				elem := l1v.Index(i)
+				if !in(r.Interface(), elem.Interface()) {
+					r = reflect.Append(r, elem)
+				}
+			}
+
+			for j := 0; j < l2v.Len(); j++ {
+				elem := l2v.Index(j)
+				if !in(r.Interface(), elem.Interface()) {
+					r = reflect.Append(r, elem)
+				}
+			}
+
+			return r.Interface(), nil
+		default:
+			return nil, errors.New("can't iterate over " + reflect.ValueOf(l2).Type().String())
+		}
+	default:
+		return nil, errors.New("can't iterate over " + reflect.ValueOf(l1).Type().String())
+	}
+}
+
 type imageHandler struct {
 	imageConfigCache map[string]image.Config
 	sync.RWMutex
@@ -2193,6 +2242,7 @@
 		"time":         asTime,
 		"trim":         trim,
 		"truncate":     truncate,
+		"union":        union,
 		"upper":        upper,
 		"urlize":       t.PathSpec.URLize,
 		"where":        where,
--- a/tpl/tplimpl/template_funcs_test.go
+++ b/tpl/tplimpl/template_funcs_test.go
@@ -184,6 +184,7 @@
 truncate: {{ "this is a very long text" | truncate 10 " ..." }}
 truncate: {{ "With [Markdown](/markdown) inside." | markdownify | truncate 14 }}
 upper: {{upper "BatMan"}}
+union: {{ union (slice 1 2 3) (slice 3 4 5) }}
 urlize: {{ "Bat Man" | urlize }}
 `
 
@@ -260,6 +261,7 @@
 truncate: this is a ...
 truncate: With <a href="/markdown">Markdown …</a>
 upper: BATMAN
+union: [1 2 3 4 5]
 urlize: bat-man
 `
 
@@ -988,8 +990,6 @@
 		{[]string{"a", "b"}, []string{"a", "b", "c"}, []string{"a", "b"}},
 		{[]string{"a", "b", "c"}, []string{"d", "e"}, []string{}},
 		{[]string{}, []string{}, []string{}},
-		{[]string{"a", "b"}, nil, make([]interface{}, 0)},
-		{nil, []string{"a", "b"}, make([]interface{}, 0)},
 		{nil, nil, make([]interface{}, 0)},
 		{[]string{"1", "2"}, []int{1, 2}, []string{}},
 		{[]int{1, 2}, []string{"1", "2"}, []int{}},
@@ -1015,6 +1015,53 @@
 	}
 
 	_, err2 := intersect([]string{"a"}, "not an array or slice")
+
+	if err2 == nil {
+		t.Error("Expected error for non array as second arg")
+	}
+}
+
+func TestUnion(t *testing.T) {
+	t.Parallel()
+	for i, this := range []struct {
+		sequence1 interface{}
+		sequence2 interface{}
+		expect    interface{}
+		isErr     bool
+	}{
+		{[]string{"a", "b", "c", "c"}, []string{"a", "b", "b"}, []string{"a", "b", "c"}, false},
+		{[]string{"a", "b"}, []string{"a", "b", "c"}, []string{"a", "b", "c"}, false},
+		{[]string{"a", "b", "c"}, []string{"d", "e"}, []string{"a", "b", "c", "d", "e"}, false},
+		{[]string{}, []string{}, []string{}, false},
+		{[]string{"a", "b"}, nil, []string{"a", "b"}, false},
+		{nil, []string{"a", "b"}, []string{"a", "b"}, false},
+		{nil, nil, make([]interface{}, 0), true},
+		{[]string{"1", "2"}, []int{1, 2}, make([]string, 0), false},
+		{[]int{1, 2}, []string{"1", "2"}, make([]int, 0), false},
+		{[]int{1, 2, 3}, []int{3, 4, 5}, []int{1, 2, 3, 4, 5}, false},
+		{[]int{1, 2, 3}, []int{1, 2, 3}, []int{1, 2, 3}, false},
+		{[]int{1, 2, 4}, []int{2, 4}, []int{1, 2, 4}, false},
+		{[]int{2, 4}, []int{1, 2, 4}, []int{2, 4, 1}, false},
+		{[]int{1, 2, 4}, []int{3, 6}, []int{1, 2, 4, 3, 6}, false},
+		{[]float64{2.2, 4.4}, []float64{1.1, 2.2, 4.4}, []float64{2.2, 4.4, 1.1}, false},
+	} {
+		results, err := union(this.sequence1, this.sequence2)
+		if err != nil && !this.isErr {
+			t.Errorf("[%d] failed: %s", i, err)
+			continue
+		}
+		if !reflect.DeepEqual(results, this.expect) && !this.isErr {
+			t.Errorf("[%d] got %v but expected %v", i, results, this.expect)
+		}
+	}
+
+	_, err1 := union("not an array or slice", []string{"a"})
+
+	if err1 == nil {
+		t.Error("Expected error for non array as first arg")
+	}
+
+	_, err2 := union([]string{"a"}, "not an array or slice")
 
 	if err2 == nil {
 		t.Error("Expected error for non array as second arg")