diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2018-07-06 14:12:10 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2018-07-06 17:51:38 +0200 |
commit | 2b8d907ab731627f4e2a30442cd729064516c8bb (patch) | |
tree | 2f9235dfbcf116c463b75d59c8be1be53d55b05a /common | |
parent | 43338c3a99769eb7d0df0c12559b8b3d42b67dba (diff) |
Add a newScratch template func
Fixes #4685
Diffstat (limited to 'common')
-rw-r--r-- | common/maps/scratch.go | 135 | ||||
-rw-r--r-- | common/maps/scratch_test.go | 170 | ||||
-rw-r--r-- | common/math/math.go | 135 | ||||
-rw-r--r-- | common/math/math_test.go | 109 |
4 files changed, 549 insertions, 0 deletions
diff --git a/common/maps/scratch.go b/common/maps/scratch.go new file mode 100644 index 000000000..d00971435 --- /dev/null +++ b/common/maps/scratch.go @@ -0,0 +1,135 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package maps + +import ( + "reflect" + "sort" + "sync" + + "github.com/gohugoio/hugo/common/math" +) + +// Scratch is a writable context used for stateful operations in Page/Node rendering. +type Scratch struct { + values map[string]interface{} + mu sync.RWMutex +} + +// Add will, for single values, add (using the + operator) the addend to the existing addend (if found). +// Supports numeric values and strings. +// +// If the first add for a key is an array or slice, then the next value(s) will be appended. +func (c *Scratch) Add(key string, newAddend interface{}) (string, error) { + + var newVal interface{} + c.mu.RLock() + existingAddend, found := c.values[key] + c.mu.RUnlock() + if found { + var err error + + addendV := reflect.ValueOf(existingAddend) + + if addendV.Kind() == reflect.Slice || addendV.Kind() == reflect.Array { + nav := reflect.ValueOf(newAddend) + if nav.Kind() == reflect.Slice || nav.Kind() == reflect.Array { + newVal = reflect.AppendSlice(addendV, nav).Interface() + } else { + newVal = reflect.Append(addendV, nav).Interface() + } + } else { + newVal, err = math.DoArithmetic(existingAddend, newAddend, '+') + if err != nil { + return "", err + } + } + } else { + newVal = newAddend + } + c.mu.Lock() + c.values[key] = newVal + c.mu.Unlock() + return "", nil // have to return something to make it work with the Go templates +} + +// Set stores a value with the given key in the Node context. +// This value can later be retrieved with Get. +func (c *Scratch) Set(key string, value interface{}) string { + c.mu.Lock() + c.values[key] = value + c.mu.Unlock() + return "" +} + +// Reset deletes the given key +func (c *Scratch) Delete(key string) string { + c.mu.Lock() + delete(c.values, key) + c.mu.Unlock() + return "" +} + +// Get returns a value previously set by Add or Set +func (c *Scratch) Get(key string) interface{} { + c.mu.RLock() + val := c.values[key] + c.mu.RUnlock() + + return val +} + +// SetInMap stores a value to a map with the given key in the Node context. +// This map can later be retrieved with GetSortedMapValues. +func (c *Scratch) SetInMap(key string, mapKey string, value interface{}) string { + c.mu.Lock() + _, found := c.values[key] + if !found { + c.values[key] = make(map[string]interface{}) + } + + c.values[key].(map[string]interface{})[mapKey] = value + c.mu.Unlock() + return "" +} + +// GetSortedMapValues returns a sorted map previously filled with SetInMap +func (c *Scratch) GetSortedMapValues(key string) interface{} { + c.mu.RLock() + + if c.values[key] == nil { + c.mu.RUnlock() + return nil + } + + unsortedMap := c.values[key].(map[string]interface{}) + c.mu.RUnlock() + var keys []string + for mapKey := range unsortedMap { + keys = append(keys, mapKey) + } + + sort.Strings(keys) + + sortedArray := make([]interface{}, len(unsortedMap)) + for i, mapKey := range keys { + sortedArray[i] = unsortedMap[mapKey] + } + + return sortedArray +} + +func NewScratch() *Scratch { + return &Scratch{values: make(map[string]interface{})} +} diff --git a/common/maps/scratch_test.go b/common/maps/scratch_test.go new file mode 100644 index 000000000..8397ba830 --- /dev/null +++ b/common/maps/scratch_test.go @@ -0,0 +1,170 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package maps + +import ( + "reflect" + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestScratchAdd(t *testing.T) { + t.Parallel() + scratch := NewScratch() + scratch.Add("int1", 10) + scratch.Add("int1", 20) + scratch.Add("int2", 20) + + assert.Equal(t, int64(30), scratch.Get("int1")) + assert.Equal(t, 20, scratch.Get("int2")) + + scratch.Add("float1", float64(10.5)) + scratch.Add("float1", float64(20.1)) + + assert.Equal(t, float64(30.6), scratch.Get("float1")) + + scratch.Add("string1", "Hello ") + scratch.Add("string1", "big ") + scratch.Add("string1", "World!") + + assert.Equal(t, "Hello big World!", scratch.Get("string1")) + + scratch.Add("scratch", scratch) + _, err := scratch.Add("scratch", scratch) + + if err == nil { + t.Errorf("Expected error from invalid arithmetic") + } + +} + +func TestScratchAddSlice(t *testing.T) { + t.Parallel() + scratch := NewScratch() + + _, err := scratch.Add("intSlice", []int{1, 2}) + assert.Nil(t, err) + _, err = scratch.Add("intSlice", 3) + assert.Nil(t, err) + + sl := scratch.Get("intSlice") + expected := []int{1, 2, 3} + + if !reflect.DeepEqual(expected, sl) { + t.Errorf("Slice difference, go %q expected %q", sl, expected) + } + + _, err = scratch.Add("intSlice", []int{4, 5}) + + assert.Nil(t, err) + + sl = scratch.Get("intSlice") + expected = []int{1, 2, 3, 4, 5} + + if !reflect.DeepEqual(expected, sl) { + t.Errorf("Slice difference, go %q expected %q", sl, expected) + } + +} + +func TestScratchSet(t *testing.T) { + t.Parallel() + scratch := NewScratch() + scratch.Set("key", "val") + assert.Equal(t, "val", scratch.Get("key")) +} + +func TestScratchDelete(t *testing.T) { + t.Parallel() + scratch := NewScratch() + scratch.Set("key", "val") + scratch.Delete("key") + scratch.Add("key", "Lucy Parsons") + assert.Equal(t, "Lucy Parsons", scratch.Get("key")) +} + +// Issue #2005 +func TestScratchInParallel(t *testing.T) { + var wg sync.WaitGroup + scratch := NewScratch() + key := "counter" + scratch.Set(key, int64(1)) + for i := 1; i <= 10; i++ { + wg.Add(1) + go func(j int) { + for k := 0; k < 10; k++ { + newVal := int64(k + j) + + _, err := scratch.Add(key, newVal) + if err != nil { + t.Errorf("Got err %s", err) + } + + scratch.Set(key, newVal) + + val := scratch.Get(key) + + if counter, ok := val.(int64); ok { + if counter < 1 { + t.Errorf("Got %d", counter) + } + } else { + t.Errorf("Got %T", val) + } + } + wg.Done() + }(i) + } + wg.Wait() +} + +func TestScratchGet(t *testing.T) { + t.Parallel() + scratch := NewScratch() + nothing := scratch.Get("nothing") + if nothing != nil { + t.Errorf("Should not return anything, but got %v", nothing) + } +} + +func TestScratchSetInMap(t *testing.T) { + t.Parallel() + scratch := NewScratch() + scratch.SetInMap("key", "lux", "Lux") + scratch.SetInMap("key", "abc", "Abc") + scratch.SetInMap("key", "zyx", "Zyx") + scratch.SetInMap("key", "abc", "Abc (updated)") + scratch.SetInMap("key", "def", "Def") + assert.Equal(t, []interface{}{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "Zyx"}, scratch.GetSortedMapValues("key")) +} + +func TestScratchGetSortedMapValues(t *testing.T) { + t.Parallel() + scratch := NewScratch() + nothing := scratch.GetSortedMapValues("nothing") + if nothing != nil { + t.Errorf("Should not return anything, but got %v", nothing) + } +} + +func BenchmarkScratchGet(b *testing.B) { + scratch := NewScratch() + scratch.Add("A", 1) + b.ResetTimer() + for i := 0; i < b.N; i++ { + scratch.Get("A") + } +} diff --git a/common/math/math.go b/common/math/math.go new file mode 100644 index 000000000..3c5ef1f9d --- /dev/null +++ b/common/math/math.go @@ -0,0 +1,135 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package math + +import ( + "errors" + "reflect" +) + +// DoArithmetic performs arithmetic operations (+,-,*,/) using reflection to +// determine the type of the two terms. +func DoArithmetic(a, b interface{}, op rune) (interface{}, error) { + av := reflect.ValueOf(a) + bv := reflect.ValueOf(b) + var ai, bi int64 + var af, bf float64 + var au, bu uint64 + switch av.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + ai = av.Int() + switch bv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + bi = bv.Int() + case reflect.Float32, reflect.Float64: + af = float64(ai) // may overflow + ai = 0 + bf = bv.Float() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + bu = bv.Uint() + if ai >= 0 { + au = uint64(ai) + ai = 0 + } else { + bi = int64(bu) // may overflow + bu = 0 + } + default: + return nil, errors.New("Can't apply the operator to the values") + } + case reflect.Float32, reflect.Float64: + af = av.Float() + switch bv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + bf = float64(bv.Int()) // may overflow + case reflect.Float32, reflect.Float64: + bf = bv.Float() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + bf = float64(bv.Uint()) // may overflow + default: + return nil, errors.New("Can't apply the operator to the values") + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + au = av.Uint() + switch bv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + bi = bv.Int() + if bi >= 0 { + bu = uint64(bi) + bi = 0 + } else { + ai = int64(au) // may overflow + au = 0 + } + case reflect.Float32, reflect.Float64: + af = float64(au) // may overflow + au = 0 + bf = bv.Float() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + bu = bv.Uint() + default: + return nil, errors.New("Can't apply the operator to the values") + } + case reflect.String: + as := av.String() + if bv.Kind() == reflect.String && op == '+' { + bs := bv.String() + return as + bs, nil + } + return nil, errors.New("Can't apply the operator to the values") + default: + return nil, errors.New("Can't apply the operator to the values") + } + + switch op { + case '+': + if ai != 0 || bi != 0 { + return ai + bi, nil + } else if af != 0 || bf != 0 { + return af + bf, nil + } else if au != 0 || bu != 0 { + return au + bu, nil + } + return 0, nil + case '-': + if ai != 0 || bi != 0 { + return ai - bi, nil + } else if af != 0 || bf != 0 { + return af - bf, nil + } else if au != 0 || bu != 0 { + return au - bu, nil + } + return 0, nil + case '*': + if ai != 0 || bi != 0 { + return ai * bi, nil + } else if af != 0 || bf != 0 { + return af * bf, nil + } else if au != 0 || bu != 0 { + return au * bu, nil + } + return 0, nil + case '/': + if bi != 0 { + return ai / bi, nil + } else if bf != 0 { + return af / bf, nil + } else if bu != 0 { + return au / bu, nil + } + return nil, errors.New("Can't divide the value by 0") + default: + return nil, errors.New("There is no such an operation") + } +} diff --git a/common/math/math_test.go b/common/math/math_test.go new file mode 100644 index 000000000..613ac3073 --- /dev/null +++ b/common/math/math_test.go @@ -0,0 +1,109 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package math + +import ( + "fmt" + "testing" + + "github.com/alecthomas/assert" + "github.com/stretchr/testify/require" +) + +func TestDoArithmetic(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + a interface{} + b interface{} + op rune + expect interface{} + }{ + {3, 2, '+', int64(5)}, + {3, 2, '-', int64(1)}, + {3, 2, '*', int64(6)}, + {3, 2, '/', int64(1)}, + {3.0, 2, '+', float64(5)}, + {3.0, 2, '-', float64(1)}, + {3.0, 2, '*', float64(6)}, + {3.0, 2, '/', float64(1.5)}, + {3, 2.0, '+', float64(5)}, + {3, 2.0, '-', float64(1)}, + {3, 2.0, '*', float64(6)}, + {3, 2.0, '/', float64(1.5)}, + {3.0, 2.0, '+', float64(5)}, + {3.0, 2.0, '-', float64(1)}, + {3.0, 2.0, '*', float64(6)}, + {3.0, 2.0, '/', float64(1.5)}, + {uint(3), uint(2), '+', uint64(5)}, + {uint(3), uint(2), '-', uint64(1)}, + {uint(3), uint(2), '*', uint64(6)}, + {uint(3), uint(2), '/', uint64(1)}, + {uint(3), 2, '+', uint64(5)}, + {uint(3), 2, '-', uint64(1)}, + {uint(3), 2, '*', uint64(6)}, + {uint(3), 2, '/', uint64(1)}, + {3, uint(2), '+', uint64(5)}, + {3, uint(2), '-', uint64(1)}, + {3, uint(2), '*', uint64(6)}, + {3, uint(2), '/', uint64(1)}, + {uint(3), -2, '+', int64(1)}, + {uint(3), -2, '-', int64(5)}, + {uint(3), -2, '*', int64(-6)}, + {uint(3), -2, '/', int64(-1)}, + {-3, uint(2), '+', int64(-1)}, + {-3, uint(2), '-', int64(-5)}, + {-3, uint(2), '*', int64(-6)}, + {-3, uint(2), '/', int64(-1)}, + {uint(3), 2.0, '+', float64(5)}, + {uint(3), 2.0, '-', float64(1)}, + {uint(3), 2.0, '*', float64(6)}, + {uint(3), 2.0, '/', float64(1.5)}, + {3.0, uint(2), '+', float64(5)}, + {3.0, uint(2), '-', float64(1)}, + {3.0, uint(2), '*', float64(6)}, + {3.0, uint(2), '/', float64(1.5)}, + {0, 0, '+', 0}, + {0, 0, '-', 0}, + {0, 0, '*', 0}, + {"foo", "bar", '+', "foobar"}, + {3, 0, '/', false}, + {3.0, 0, '/', false}, + {3, 0.0, '/', false}, + {uint(3), uint(0), '/', false}, + {3, uint(0), '/', false}, + {-3, uint(0), '/', false}, + {uint(3), 0, '/', false}, + {3.0, uint(0), '/', false}, + {uint(3), 0.0, '/', false}, + {3, "foo", '+', false}, + {3.0, "foo", '+', false}, + {uint(3), "foo", '+', false}, + {"foo", 3, '+', false}, + {"foo", "bar", '-', false}, + {3, 2, '%', false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := DoArithmetic(test.a, test.b, test.op) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} |