diff --git a/lang/funcs/number.go b/lang/funcs/number.go index 15cfe7182a1c..c813f47bf67e 100644 --- a/lang/funcs/number.go +++ b/lang/funcs/number.go @@ -2,6 +2,7 @@ package funcs import ( "math" + "math/big" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" @@ -128,6 +129,62 @@ var SignumFunc = function.New(&function.Spec{ }, }) +// ParseIntFunc contructs a function that parses a string argument and returns an integer of the specified base. +var ParseIntFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "number", + Type: cty.DynamicPseudoType, + }, + { + Name: "base", + Type: cty.Number, + }, + }, + + Type: func(args []cty.Value) (cty.Type, error) { + if !args[0].Type().Equals(cty.String) { + return cty.Number, function.NewArgErrorf(0, "first argument must be a string, not %s", args[0].Type().FriendlyName()) + } + return cty.Number, nil + }, + + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + var numstr string + var base int + var err error + + if err = gocty.FromCtyValue(args[0], &numstr); err != nil { + return cty.UnknownVal(cty.String), function.NewArgError(0, err) + } + + if err = gocty.FromCtyValue(args[1], &base); err != nil { + return cty.UnknownVal(cty.Number), function.NewArgError(1, err) + } + + if base < 2 || base > 62 { + return cty.UnknownVal(cty.Number), function.NewArgErrorf( + 1, + "base must be a whole number between 2 and 62 inclusive", + ) + } + + num, ok := (&big.Int{}).SetString(numstr, base) + if !ok { + return cty.UnknownVal(cty.Number), function.NewArgErrorf( + 0, + "cannot parse %q as a base %d integer", + numstr, + base, + ) + } + + parsedNum := cty.NumberVal((&big.Float{}).SetInt(num)) + + return parsedNum, nil + }, +}) + // Ceil returns the closest whole number greater than or equal to the given value. func Ceil(num cty.Value) (cty.Value, error) { return CeilFunc.Call([]cty.Value{num}) @@ -153,3 +210,8 @@ func Pow(num, power cty.Value) (cty.Value, error) { func Signum(num cty.Value) (cty.Value, error) { return SignumFunc.Call([]cty.Value{num}) } + +// ParseInt parses a string argument and returns an integer of the specified base. +func ParseInt(num cty.Value, base cty.Value) (cty.Value, error) { + return ParseIntFunc.Call([]cty.Value{num, base}) +} diff --git a/lang/funcs/number_test.go b/lang/funcs/number_test.go index 25b91e7e4385..97ec70a75c59 100644 --- a/lang/funcs/number_test.go +++ b/lang/funcs/number_test.go @@ -257,3 +257,164 @@ func TestSignum(t *testing.T) { }) } } + +func TestParseInt(t *testing.T) { + tests := []struct { + Num cty.Value + Base cty.Value + Want cty.Value + Err bool + }{ + { + cty.StringVal("128"), + cty.NumberIntVal(10), + cty.NumberIntVal(128), + false, + }, + { + cty.StringVal("-128"), + cty.NumberIntVal(10), + cty.NumberIntVal(-128), + false, + }, + { + cty.StringVal("00128"), + cty.NumberIntVal(10), + cty.NumberIntVal(128), + false, + }, + { + cty.StringVal("-00128"), + cty.NumberIntVal(10), + cty.NumberIntVal(-128), + false, + }, + { + cty.StringVal("FF00"), + cty.NumberIntVal(16), + cty.NumberIntVal(65280), + false, + }, + { + cty.StringVal("ff00"), + cty.NumberIntVal(16), + cty.NumberIntVal(65280), + false, + }, + { + cty.StringVal("-FF00"), + cty.NumberIntVal(16), + cty.NumberIntVal(-65280), + false, + }, + { + cty.StringVal("00FF00"), + cty.NumberIntVal(16), + cty.NumberIntVal(65280), + false, + }, + { + cty.StringVal("-00FF00"), + cty.NumberIntVal(16), + cty.NumberIntVal(-65280), + false, + }, + { + cty.StringVal("1011111011101111"), + cty.NumberIntVal(2), + cty.NumberIntVal(48879), + false, + }, + { + cty.StringVal("aA"), + cty.NumberIntVal(62), + cty.NumberIntVal(656), + false, + }, + { + cty.StringVal("Aa"), + cty.NumberIntVal(62), + cty.NumberIntVal(2242), + false, + }, + { + cty.StringVal("999999999999999999999999999999999999999999999999999999999999"), + cty.NumberIntVal(10), + cty.MustParseNumberVal("999999999999999999999999999999999999999999999999999999999999"), + false, + }, + { + cty.StringVal("FF"), + cty.NumberIntVal(10), + cty.UnknownVal(cty.Number), + true, + }, + { + cty.StringVal("00FF"), + cty.NumberIntVal(10), + cty.UnknownVal(cty.Number), + true, + }, + { + cty.StringVal("-00FF"), + cty.NumberIntVal(10), + cty.UnknownVal(cty.Number), + true, + }, + { + cty.NumberIntVal(2), + cty.NumberIntVal(10), + cty.UnknownVal(cty.Number), + true, + }, + { + cty.StringVal("1"), + cty.NumberIntVal(63), + cty.UnknownVal(cty.Number), + true, + }, + { + cty.StringVal("1"), + cty.NumberIntVal(-1), + cty.UnknownVal(cty.Number), + true, + }, + { + cty.StringVal("1"), + cty.NumberIntVal(1), + cty.UnknownVal(cty.Number), + true, + }, + { + cty.StringVal("1"), + cty.NumberIntVal(0), + cty.UnknownVal(cty.Number), + true, + }, + { + cty.StringVal("1.2"), + cty.NumberIntVal(10), + cty.UnknownVal(cty.Number), + true, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("parseint(%#v, %#v)", test.Num, test.Base), func(t *testing.T) { + got, err := ParseInt(test.Num, test.Base) + + 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) + } + }) + } +} diff --git a/lang/functions.go b/lang/functions.go index abd76c09d237..602b23daad68 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -86,6 +86,7 @@ func (s *Scope) Functions() map[string]function.Function { "md5": funcs.Md5Func, "merge": funcs.MergeFunc, "min": stdlib.MinFunc, + "parseint": funcs.ParseIntFunc, "pathexpand": funcs.PathExpandFunc, "pow": funcs.PowFunc, "range": stdlib.RangeFunc, diff --git a/lang/functions_test.go b/lang/functions_test.go index ac0ff91d97d4..64652ab5882c 100644 --- a/lang/functions_test.go +++ b/lang/functions_test.go @@ -544,6 +544,13 @@ func TestFunctions(t *testing.T) { }, }, + "parseint": { + { + `parseint("100", 10)`, + cty.NumberIntVal(100), + }, + }, + "pathexpand": { { `pathexpand("~/test-file")`, diff --git a/website/docs/configuration/functions/parseint.html.md b/website/docs/configuration/functions/parseint.html.md new file mode 100644 index 000000000000..44c7a9cfa4b6 --- /dev/null +++ b/website/docs/configuration/functions/parseint.html.md @@ -0,0 +1,55 @@ +--- +layout: "functions" +page_title: "parseint - Functions - Configuration Language" +sidebar_current: "docs-funcs-numeric-parseint" +description: |- + The parseint function parses the given string as a representation of an integer. +--- + +# `parseint` Function + +-> **Note:** This page is about Terraform 0.12 and later. For Terraform 0.11 and +earlier, see +[0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html). + +`parseint` parses the given string as a representation of an integer in +the specified base and returns the resulting number. The base must be between 2 +and 62 inclusive. + +All bases use the arabic numerals 0 through 9 first. Bases between 11 and 36 +inclusive use case-insensitive latin letters to represent higher unit values. +Bases 37 and higher use lowercase latin letters and then uppercase latin +letters. + +If the given string contains any non-digit characters or digit characters that +are too large for the given base then `parseint` will produce an error. + +## Examples + +``` +> parseint("100", 10) +100 + +> parseint("FF", 16) +255 + +> parseint("-10", 16) +-16 + +> parseint("1011111011101111", 2) +48879 + +> parseint("aA", 62) +656 + +> parseint("12", 2) + +Error: Invalid function argument + +Invalid value for "number" parameter: cannot parse "12" as a base 2 integer. +``` + +## Related Functions + +* [`format`](./format.html) can format numbers and other values into strings, + with optional zero padding, alignment, etc. diff --git a/website/layouts/functions.erb b/website/layouts/functions.erb index 020b6cf11ecc..059228dfe5ce 100644 --- a/website/layouts/functions.erb +++ b/website/layouts/functions.erb @@ -40,6 +40,10 @@ min +
  • + parseint +
  • +
  • pow