Repository: gohugoio/hugo (language: go)

Issue:
tpl/math: Allow variadic math functions to take slice args
https://github.com/gohugoio/hugo/pull/10638 changed `add`, `sub`, `mul`, `div`, `main`, and `max` into variadic functions. So you can do this:

```
{{ math.Min 6 7 42 }}
{{ math.Max 6 7 42 }}
{{ math.Add 6 7 42 }}
{{ math.Mul 6 7 42 }}
```

I'm finding this would be _much_ more useful:

```
{{ math.Min (slice 6 7 42) }}
{{ math.Max (slice 6 7 42) }}
{{ math.Add (slice 6 7 42) }}
{{ math.Mul (slice 6 7 42) }}
```

And these should be valid as well:

```
{{ math.Min 6 7 }}
{{ math.Min (slice 6) 7 }}
{{ math.Min (slice 6) (slice 7) }}
{{ math.Min (slice 6) (slice 7) 42 }}
```

I suppose we should do the same with `math.Sub` and `math.Div`.

The recently implemented `urls.PathJoin` follows this model.

They should still throw an error with a single arg. For example

```
{{ math.Sub 6 }} --> error
{{ math.Sub (slice 6) }} --> error
```




Discussion / hints:
In retrospect, we should have:

1. Not changed `math.Add`, `math.Sub`, `math.Mul`, and `math.Div` (require 2 scalar values)
2. Changed `math.Min`and `math.Max` (accept 1 or more values, scalar or slice, or combination of the two)
3. Introduced `math.Sum` (1 or more values, scalar or slice, or combination of the two)
4. Introduced `math.Product` (1 or more values, scalar or slice, or combination of the two)

Excel has `sum` and `product` functions, but does not have variadic functions for subtraction or division. I suspect that's because they aren't terribly useful.

Items 2-4 would be backwards compatible. Thoughts?
I'm a little slow tonight, but why couldn't we make

{{ math.Add 6 7 42 }}
{{ math.Add (slice 6 7 42) }}

Both work?

My thinking was:

`math.Add` should error if < 2 args (e.g., `math.Add 6` doesn't make sense -- this is the current behavior)
`math.Sub` should error if < 2 args (e.g., `math.Sub 6` doesn't make sense -- this is the current behavior)
`math.Mul` should error if < 2 args (e.g., `math.Mul 6` doesn't make sense -- this is the current behavior)
`math.Div` should error if < 2 args (e.g., `math.Div 6` doesn't make sense -- this is the current behavior)

`math.Sum` should error if < 1 arg
`math.Product` should error if < 1 arg
`math.Min` should error if < 1 arg
`math.Max` should error if < 1 arg

When building a slice for `math.Sum`, `math.Product`, `math.Min`, or `math.Max` through iteration, it is possible to have just one iteration (e.g., ranging through a section with only one page).


>math.Add should error if < 2 args (e.g., math.Add 6 doesn't make sense -- this is the current behavior)

Sure, but it shouldn't be too hard to expand on that logic and define args as either `number of args or, if number of arg is 1 and the arg is a slice, number of values in the slice`.

So I agree that we should do what this issue describes, but we cannot (and I don't see why we would have to) remove the option to say e.g. `{{ math.Add 1 2 }}`.



If I understand correctly, you are suggesting:

```
{{ math.Add 6 }} --> error (this is the current behavior, needs > 1 arg)

{{ math.Add (slice 6) }} --> 6
```

I find that confusing for a couple of reasons:

1. To me, `math.Add` is (conceptually) the `+` operator, and neither `6 +` nor `[6] +`  seem right
2. The documentation would be something like, "Requires 2 or more arguments, or 1 argument if it is a slice of one or elements"

My preference is to:

1. Leave `math.Add`, `math.Sub`, `math.Mul`, and `math.Div` as they are
2. Update `math.Min` and `math.Max` to allow 1 or more args, either scalar or slice, or combination of the two
3. Add `math.Sum` (allow 1 or more args, either scalar or slice, or combination of the two)
4. Add `math.Product` (allow 1 or more args, either scalar or slice, or combination of the two)

This is backwards compatible.
```
{{ math.Add 6 }} --> error (this is the current behavior, needs > 1 arg)

{{ math.Add (slice 6) }} --> 6
```

I think this is easier to reason about if you say that if 1 argument and that argument is a slice, then "unpack" it and then apply regular validation rules (which is a way of simulating varg with slice).

With that 

{{ math.Add (slice 6) }}  => {{ math.Add 6 }} => error (this is the current behavior, needs > 1 arg)
{{ math.Add (slice 6 7) }}  => {{ math.Add 6 7 }} => OK.


I'm not sure your examples above ala

{{ math.Min (slice 6) (slice 7) }}

Makes much sense to me. I think we should focus on the main problem in this issue: That it's not currently a way to do `math.Add(myslice...)` in Go templates (which is a very common construct in Go).







Sorry, I'm not making myself clear.

```
{{ $vals := slice }}
{{ range .Pages }}
  {{ $vals := $vals | append .Params.val }}
{{ end }}

{{ math.Sum $vals }}
{{ math.Product $vals }}
{{ math.Min $vals }}
{{ math.Max $vals }}
```


I don't know how many pages are in the collection. Could be 0, 1, 2, etc. The functions should not error with 1 arg.








To get the discussion back on track. I agree about what you say here:

```
Leave math.Add, math.Sub, math.Mul, and math.Div as they are
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)
```

Fix patch:
diff --git a/tpl/math/math.go b/tpl/math/math.go
index d73f212a658..a921bd7ada3 100644
--- a/tpl/math/math.go
+++ b/tpl/math/math.go
@@ -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


Test patch:
diff --git a/tpl/math/math_test.go b/tpl/math/math_test.go
index fad86938df0..5b54b6ac85e 100644
--- a/tpl/math/math_test.go
+++ b/tpl/math/math_test.go
@@ -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))
+
+}