]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
tpl/math: Allow variadic math functions to take slice args, add math.Product, math.Sum
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 13 Jun 2023 18:00:00 +0000 (20:00 +0200)
committerGitHub <noreply@github.com>
Tue, 13 Jun 2023 18:00:00 +0000 (20:00 +0200)
* Update math.Min and math.Max to allow 1 or more args, either scalar or slice, or combination of the two
* Add math.Sum (allow 1 or more args, either scalar or slice, or combination of the two)
* Add math.Product (allow 1 or more args, either scalar or slice, or combination of the two)

Fixes #11030

tpl/math/math.go
tpl/math/math_test.go

index d73f212a658904ce8ca33be6deee291ee3051f02..a921bd7ada3a9b8c8b734e631f4932e7da3bb4a7 100644 (file)
@@ -16,7 +16,9 @@ package math
 
 import (
        "errors"
+       "fmt"
        "math"
+       "reflect"
        "sync/atomic"
 
        _math "github.com/gohugoio/hugo/common/math"
@@ -25,6 +27,7 @@ import (
 
 var (
        errMustTwoNumbersError = errors.New("must provide at least two numbers")
+       errMustOneNumberError  = errors.New("must provide at least one number")
 )
 
 // New returns a new instance of the math-namespaced template functions.
@@ -85,48 +88,30 @@ func (ns *Namespace) Log(n any) (float64, error) {
        return math.Log(af), nil
 }
 
-// Max returns the greater of the multivalued numbers n1 and n2 or more values.
+// Max returns the greater of all numbers in inputs. Any slices in inputs are flattened.
 func (ns *Namespace) Max(inputs ...any) (maximum float64, err error) {
-       if len(inputs) < 2 {
-               err = errMustTwoNumbersError
-               return
-       }
-       var value float64
-       for index, input := range inputs {
-               value, err = cast.ToFloat64E(input)
-               if err != nil {
-                       err = errors.New("Max operator can't be used with non-float value")
-                       return
-               }
-               if index == 0 {
-                       maximum = value
-                       continue
-               }
-               maximum = math.Max(value, maximum)
-       }
-       return
+       return ns.applyOpToScalarsOrSlices("Max", math.Max, inputs...)
 }
 
-// Min returns the smaller of multivalued numbers n1 and n2 or more values.
+// Min returns the smaller of all numbers in inputs. Any slices in inputs are flattened.
 func (ns *Namespace) Min(inputs ...any) (minimum float64, err error) {
-       if len(inputs) < 2 {
-               err = errMustTwoNumbersError
-               return
+       return ns.applyOpToScalarsOrSlices("Min", math.Min, inputs...)
+}
+
+// Sum returns the sum of all numbers in inputs. Any slices in inputs are flattened.
+func (ns *Namespace) Sum(inputs ...any) (sum float64, err error) {
+       fn := func(x, y float64) float64 {
+               return x + y
        }
-       var value float64
-       for index, input := range inputs {
-               value, err = cast.ToFloat64E(input)
-               if err != nil {
-                       err = errors.New("Max operator can't be used with non-float value")
-                       return
-               }
-               if index == 0 {
-                       minimum = value
-                       continue
-               }
-               minimum = math.Min(value, minimum)
+       return ns.applyOpToScalarsOrSlices("Sum", fn, inputs...)
+}
+
+// Product returns the product of all numbers in inputs. Any slices in inputs are flattened.
+func (ns *Namespace) Product(inputs ...any) (product float64, err error) {
+       fn := func(x, y float64) float64 {
+               return x * y
        }
-       return
+       return ns.applyOpToScalarsOrSlices("Product", fn, inputs...)
 }
 
 // Mod returns n1 % n2.
@@ -197,6 +182,58 @@ func (ns *Namespace) Sub(inputs ...any) (any, error) {
        return ns.doArithmetic(inputs, '-')
 }
 
+func (ns *Namespace) applyOpToScalarsOrSlices(opName string, op func(x, y float64) float64, inputs ...any) (result float64, err error) {
+       var i int
+       var hasValue bool
+       for _, input := range inputs {
+               var values []float64
+               var isSlice bool
+               values, isSlice, err = ns.toFloatsE(input)
+               if err != nil {
+                       err = fmt.Errorf("%s operator can't be used with non-float values", opName)
+                       return
+               }
+               hasValue = hasValue || len(values) > 0 || isSlice
+               for _, value := range values {
+                       i++
+                       if i == 1 {
+                               result = value
+                               continue
+                       }
+                       result = op(result, value)
+               }
+       }
+
+       if !hasValue {
+               err = errMustOneNumberError
+               return
+       }
+       return
+
+}
+
+func (ns *Namespace) toFloatsE(v any) ([]float64, bool, error) {
+       vv := reflect.ValueOf(v)
+       switch vv.Kind() {
+       case reflect.Slice, reflect.Array:
+               var floats []float64
+               for i := 0; i < vv.Len(); i++ {
+                       f, err := cast.ToFloat64E(vv.Index(i).Interface())
+                       if err != nil {
+                               return nil, true, err
+                       }
+                       floats = append(floats, f)
+               }
+               return floats, true, nil
+       default:
+               f, err := cast.ToFloat64E(v)
+               if err != nil {
+                       return nil, false, err
+               }
+               return []float64{f}, false, nil
+       }
+}
+
 func (ns *Namespace) doArithmetic(inputs []any, operation rune) (value any, err error) {
        if len(inputs) < 2 {
                return nil, errMustTwoNumbersError
index fad86938df003b58b428f7c9923ecca787d1a32e..5b54b6ac85e7492610f99b3018b6ff869431534e 100644 (file)
@@ -415,15 +415,21 @@ func TestMax(t *testing.T) {
                {[]any{0, 1}, 1.0},
                {[]any{1, -1}, 1.0},
                {[]any{1, 0}, 1.0},
+               {[]any{32}, 32.0},
                {[]any{1, 1}, 1.0},
                {[]any{1.2, 1.23}, 1.23},
                {[]any{-1.2, -1.23}, -1.2},
                {[]any{0, "a"}, false},
                {[]any{"a", 0}, false},
                {[]any{"a", "b"}, false},
-               // miss values
+               // Issue #11030
+               {[]any{7, []any{3, 4}}, 7.0},
+               {[]any{8, []any{3, 12}, 3}, 12.0},
+               {[]any{[]any{3, 5, 2}}, 5.0},
+               {[]any{3, []int{3, 6}, 3}, 6.0},
+               // No values.
                {[]any{}, false},
-               {[]any{0}, false},
+
                // multi values
                {[]any{-1, -2, -3}, -1.0},
                {[]any{1, 2, 3}, 3.0},
@@ -436,8 +442,9 @@ func TestMax(t *testing.T) {
                        continue
                }
 
-               c.Assert(err, qt.IsNil)
-               c.Assert(result, qt.Equals, test.expect)
+               msg := qt.Commentf("values: %v", test.values)
+               c.Assert(err, qt.IsNil, msg)
+               c.Assert(result, qt.Equals, test.expect, msg)
        }
 }
 
@@ -463,14 +470,21 @@ func TestMin(t *testing.T) {
                {[]any{1, -1}, -1.0},
                {[]any{1, 0}, 0.0},
                {[]any{1, 1}, 1.0},
+               {[]any{2}, 2.0},
                {[]any{1.2, 1.23}, 1.2},
                {[]any{-1.2, -1.23}, -1.23},
                {[]any{0, "a"}, false},
                {[]any{"a", 0}, false},
                {[]any{"a", "b"}, false},
-               // miss values
+               // Issue #11030
+               {[]any{1, []any{3, 4}}, 1.0},
+               {[]any{8, []any{3, 2}, 3}, 2.0},
+               {[]any{[]any{3, 2, 2}}, 2.0},
+               {[]any{8, []int{3, 2}, 3}, 2.0},
+
+               // No values.
                {[]any{}, false},
-               {[]any{0}, false},
+
                // multi values
                {[]any{-1, -2, -3}, -3.0},
                {[]any{1, 2, 3}, 1.0},
@@ -484,7 +498,54 @@ func TestMin(t *testing.T) {
                        continue
                }
 
-               c.Assert(err, qt.IsNil)
+               c.Assert(err, qt.IsNil, qt.Commentf("values: %v", test.values))
                c.Assert(result, qt.Equals, test.expect)
        }
 }
+
+func TestSum(t *testing.T) {
+       t.Parallel()
+       c := qt.New(t)
+
+       ns := New()
+
+       mustSum := func(values ...any) any {
+               result, err := ns.Sum(values...)
+               c.Assert(err, qt.IsNil)
+               return result
+       }
+
+       c.Assert(mustSum(1, 2, 3), qt.Equals, 6.0)
+       c.Assert(mustSum(1, 2, 3.0), qt.Equals, 6.0)
+       c.Assert(mustSum(1, 2, []any{3, 4}), qt.Equals, 10.0)
+       c.Assert(mustSum(23), qt.Equals, 23.0)
+       c.Assert(mustSum([]any{23}), qt.Equals, 23.0)
+       c.Assert(mustSum([]any{}), qt.Equals, 0.0)
+
+       _, err := ns.Sum()
+       c.Assert(err, qt.Not(qt.IsNil))
+
+}
+
+func TestProduct(t *testing.T) {
+       t.Parallel()
+       c := qt.New(t)
+
+       ns := New()
+
+       mustProduct := func(values ...any) any {
+               result, err := ns.Product(values...)
+               c.Assert(err, qt.IsNil)
+               return result
+       }
+
+       c.Assert(mustProduct(2, 2, 3), qt.Equals, 12.0)
+       c.Assert(mustProduct(1, 2, 3.0), qt.Equals, 6.0)
+       c.Assert(mustProduct(1, 2, []any{3, 4}), qt.Equals, 24.0)
+       c.Assert(mustProduct(3.0), qt.Equals, 3.0)
+       c.Assert(mustProduct([]string{}), qt.Equals, 0.0)
+
+       _, err := ns.Product()
+       c.Assert(err, qt.Not(qt.IsNil))
+
+}