From d4d8812afa63bca35384169ccbcb10d923a80736 Mon Sep 17 00:00:00 2001 From: Noah Mercado Date: Wed, 15 Apr 2020 14:27:06 -0400 Subject: [PATCH] Feature: Sum Function (#24666) The sum function takes a list or set of numbers and returns the sum of those numbers. --- lang/funcs/collection.go | 52 ++++++ lang/funcs/collection_test.go | 172 ++++++++++++++++++ lang/functions.go | 1 + lang/functions_test.go | 7 + .../docs/configuration/functions/sum.html.md | 24 +++ website/layouts/functions.erb | 4 + 6 files changed, 260 insertions(+) create mode 100644 website/docs/configuration/functions/sum.html.md diff --git a/lang/funcs/collection.go b/lang/funcs/collection.go index dbcc0c715..bc93f8a2c 100644 --- a/lang/funcs/collection.go +++ b/lang/funcs/collection.go @@ -461,6 +461,53 @@ var MatchkeysFunc = function.New(&function.Spec{ }, }) +// SumFunc constructs a function that returns the sum of all +// numbers provided in a list +var SumFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "list", + Type: cty.DynamicPseudoType, + }, + }, + Type: function.StaticReturnType(cty.Number), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + + if !args[0].CanIterateElements() { + return cty.NilVal, function.NewArgErrorf(0, "cannot sum noniterable") + } + + if args[0].LengthInt() == 0 { // Easy path + return cty.NilVal, function.NewArgErrorf(0, "cannot sum an empty list") + } + + arg := args[0].AsValueSlice() + ty := args[0].Type() + + var i float64 + var s float64 + + if !ty.IsListType() && !ty.IsSetType() && !ty.IsTupleType() { + return cty.NilVal, function.NewArgErrorf(0, fmt.Sprintf("argument must be list, set, or tuple. Received %s", ty.FriendlyName())) + } + + if !args[0].IsKnown() { + return cty.UnknownVal(cty.Number), nil + } + + for _, v := range arg { + + if err := gocty.FromCtyValue(v, &i); err != nil { + return cty.UnknownVal(cty.Number), function.NewArgErrorf(0, "argument must be list, set, or tuple of number values") + } else { + s += i + } + } + + return cty.NumberFloatVal(s), nil + }, +}) + // TransposeFunc constructs a function that takes a map of lists of strings and // swaps the keys and values to produce a new map of lists of strings. var TransposeFunc = function.New(&function.Spec{ @@ -570,6 +617,11 @@ func Matchkeys(values, keys, searchset cty.Value) (cty.Value, error) { return MatchkeysFunc.Call([]cty.Value{values, keys, searchset}) } +// Sum adds numbers in a list, set, or tuple +func Sum(list cty.Value) (cty.Value, error) { + return SumFunc.Call([]cty.Value{list}) +} + // Transpose takes a map of lists of strings and swaps the keys and values to // produce a new map of lists of strings. func Transpose(values cty.Value) (cty.Value, error) { diff --git a/lang/funcs/collection_test.go b/lang/funcs/collection_test.go index fa5368a46..8e15f81ee 100644 --- a/lang/funcs/collection_test.go +++ b/lang/funcs/collection_test.go @@ -1055,6 +1055,178 @@ func TestMatchkeys(t *testing.T) { } } +func TestSum(t *testing.T) { + tests := []struct { + List cty.Value + Want cty.Value + Err bool + }{ + { + cty.ListVal([]cty.Value{ + cty.NumberIntVal(1), + cty.NumberIntVal(2), + cty.NumberIntVal(3), + }), + cty.NumberIntVal(6), + false, + }, + { + cty.ListVal([]cty.Value{ + cty.NumberIntVal(1476), + cty.NumberIntVal(2093), + cty.NumberIntVal(2092495), + cty.NumberIntVal(64589234), + cty.NumberIntVal(234), + }), + cty.NumberIntVal(66685532), + false, + }, + { + cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + cty.StringVal("c"), + }), + cty.UnknownVal(cty.String), + true, + }, + { + cty.ListVal([]cty.Value{ + cty.NumberIntVal(10), + cty.NumberIntVal(-19), + cty.NumberIntVal(5), + }), + cty.NumberIntVal(-4), + false, + }, + { + cty.ListVal([]cty.Value{ + cty.NumberFloatVal(10.2), + cty.NumberFloatVal(19.4), + cty.NumberFloatVal(5.7), + }), + cty.NumberFloatVal(35.3), + false, + }, + { + cty.ListVal([]cty.Value{ + cty.NumberFloatVal(-10.2), + cty.NumberFloatVal(-19.4), + cty.NumberFloatVal(-5.7), + }), + cty.NumberFloatVal(-35.3), + false, + }, + { + cty.ListVal([]cty.Value{cty.NullVal(cty.Number)}), + cty.NilVal, + true, + }, + { + cty.SetVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + cty.StringVal("c"), + }), + cty.UnknownVal(cty.String), + true, + }, + { + cty.SetVal([]cty.Value{ + cty.NumberIntVal(10), + cty.NumberIntVal(-19), + cty.NumberIntVal(5), + }), + cty.NumberIntVal(-4), + false, + }, + { + cty.SetVal([]cty.Value{ + cty.NumberIntVal(10), + cty.NumberIntVal(25), + cty.NumberIntVal(30), + }), + cty.NumberIntVal(65), + false, + }, + { + cty.SetVal([]cty.Value{ + cty.NumberFloatVal(2340.8), + cty.NumberFloatVal(10.2), + cty.NumberFloatVal(3), + }), + cty.NumberFloatVal(2354), + false, + }, + { + cty.SetVal([]cty.Value{ + cty.NumberFloatVal(2), + }), + cty.NumberFloatVal(2), + false, + }, + { + cty.SetVal([]cty.Value{ + cty.NumberFloatVal(-2), + cty.NumberFloatVal(-50), + cty.NumberFloatVal(-20), + cty.NumberFloatVal(-123), + cty.NumberFloatVal(-4), + }), + cty.NumberFloatVal(-199), + false, + }, + { + cty.TupleVal([]cty.Value{ + cty.NumberIntVal(12), + cty.StringVal("a"), + cty.NumberIntVal(38), + }), + cty.UnknownVal(cty.String), + true, + }, + { + cty.NumberIntVal(12), + cty.NilVal, + true, + }, + { + cty.ListValEmpty(cty.Number), + cty.NilVal, + true, + }, + { + cty.MapVal(map[string]cty.Value{"hello": cty.True}), + cty.NilVal, + true, + }, + { + cty.UnknownVal(cty.Number), + cty.UnknownVal(cty.Number), + false, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("sum(%#v)", test.List), func(t *testing.T) { + got, err := Sum(test.List) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} + func TestTranspose(t *testing.T) { tests := []struct { Values cty.Value diff --git a/lang/functions.go b/lang/functions.go index d02b9fa88..b4cc2d72e 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -111,6 +111,7 @@ func (s *Scope) Functions() map[string]function.Function { "split": stdlib.SplitFunc, "strrev": stdlib.ReverseFunc, "substr": stdlib.SubstrFunc, + "sum": funcs.SumFunc, "timestamp": funcs.TimestampFunc, "timeadd": stdlib.TimeAddFunc, "title": stdlib.TitleFunc, diff --git a/lang/functions_test.go b/lang/functions_test.go index 029cc0c96..b1288f0e3 100644 --- a/lang/functions_test.go +++ b/lang/functions_test.go @@ -786,6 +786,13 @@ func TestFunctions(t *testing.T) { }, }, + "sum": { + { + `sum([2340.5,10,3])`, + cty.NumberFloatVal(2353.5), + }, + }, + "templatefile": { { `templatefile("hello.tmpl", {name = "Jodie"})`, diff --git a/website/docs/configuration/functions/sum.html.md b/website/docs/configuration/functions/sum.html.md new file mode 100644 index 000000000..20958974d --- /dev/null +++ b/website/docs/configuration/functions/sum.html.md @@ -0,0 +1,24 @@ +--- +layout: "functions" +page_title: "sum - Functions - Configuration Language" +sidebar_current: "docs-funcs-collection-sum" +description: |- + The sum function takes a list or set of numbers and returns the sum of those + numbers. +--- + +# `sum` Function + +-> **Note:** This page is about Terraform 0.13 and later. For Terraform 0.11 and +earlier, see +[0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html). + +`sum` takes a list or set of numbers and returns the sum of those numbers. + + +## Examples + +``` +> sum([10, 13, 6, 4.5]) +33.5 +``` \ No newline at end of file diff --git a/website/layouts/functions.erb b/website/layouts/functions.erb index 063347f51..12fdfdee0 100644 --- a/website/layouts/functions.erb +++ b/website/layouts/functions.erb @@ -238,6 +238,10 @@ sort +
  • + sum +
  • +
  • transpose