Skip to content

Commit f8f8e51

Browse files
authored
Merge pull request #181 from hashicorp/cherry-pick-22747
lang/funcs: parseint function (cherry-pick hashicorp/terraform#22747)
2 parents 7e1a983 + d8924fd commit f8f8e51

File tree

4 files changed

+231
-0
lines changed

4 files changed

+231
-0
lines changed

internal/lang/funcs/number.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package funcs
22

33
import (
44
"math"
5+
"math/big"
56

67
"github.com/zclconf/go-cty/cty"
78
"github.com/zclconf/go-cty/cty/function"
@@ -128,6 +129,62 @@ var SignumFunc = function.New(&function.Spec{
128129
},
129130
})
130131

132+
// ParseIntFunc contructs a function that parses a string argument and returns an integer of the specified base.
133+
var ParseIntFunc = function.New(&function.Spec{
134+
Params: []function.Parameter{
135+
{
136+
Name: "number",
137+
Type: cty.DynamicPseudoType,
138+
},
139+
{
140+
Name: "base",
141+
Type: cty.Number,
142+
},
143+
},
144+
145+
Type: func(args []cty.Value) (cty.Type, error) {
146+
if !args[0].Type().Equals(cty.String) {
147+
return cty.Number, function.NewArgErrorf(0, "first argument must be a string, not %s", args[0].Type().FriendlyName())
148+
}
149+
return cty.Number, nil
150+
},
151+
152+
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
153+
var numstr string
154+
var base int
155+
var err error
156+
157+
if err = gocty.FromCtyValue(args[0], &numstr); err != nil {
158+
return cty.UnknownVal(cty.String), function.NewArgError(0, err)
159+
}
160+
161+
if err = gocty.FromCtyValue(args[1], &base); err != nil {
162+
return cty.UnknownVal(cty.Number), function.NewArgError(1, err)
163+
}
164+
165+
if base < 2 || base > 62 {
166+
return cty.UnknownVal(cty.Number), function.NewArgErrorf(
167+
1,
168+
"base must be a whole number between 2 and 62 inclusive",
169+
)
170+
}
171+
172+
num, ok := (&big.Int{}).SetString(numstr, base)
173+
if !ok {
174+
return cty.UnknownVal(cty.Number), function.NewArgErrorf(
175+
0,
176+
"cannot parse %q as a base %d integer",
177+
numstr,
178+
base,
179+
)
180+
}
181+
182+
parsedNum := cty.NumberVal((&big.Float{}).SetInt(num))
183+
184+
return parsedNum, nil
185+
},
186+
})
187+
131188
// Ceil returns the closest whole number greater than or equal to the given value.
132189
func Ceil(num cty.Value) (cty.Value, error) {
133190
return CeilFunc.Call([]cty.Value{num})
@@ -153,3 +210,8 @@ func Pow(num, power cty.Value) (cty.Value, error) {
153210
func Signum(num cty.Value) (cty.Value, error) {
154211
return SignumFunc.Call([]cty.Value{num})
155212
}
213+
214+
// ParseInt parses a string argument and returns an integer of the specified base.
215+
func ParseInt(num cty.Value, base cty.Value) (cty.Value, error) {
216+
return ParseIntFunc.Call([]cty.Value{num, base})
217+
}

internal/lang/funcs/number_test.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,164 @@ func TestSignum(t *testing.T) {
257257
})
258258
}
259259
}
260+
261+
func TestParseInt(t *testing.T) {
262+
tests := []struct {
263+
Num cty.Value
264+
Base cty.Value
265+
Want cty.Value
266+
Err bool
267+
}{
268+
{
269+
cty.StringVal("128"),
270+
cty.NumberIntVal(10),
271+
cty.NumberIntVal(128),
272+
false,
273+
},
274+
{
275+
cty.StringVal("-128"),
276+
cty.NumberIntVal(10),
277+
cty.NumberIntVal(-128),
278+
false,
279+
},
280+
{
281+
cty.StringVal("00128"),
282+
cty.NumberIntVal(10),
283+
cty.NumberIntVal(128),
284+
false,
285+
},
286+
{
287+
cty.StringVal("-00128"),
288+
cty.NumberIntVal(10),
289+
cty.NumberIntVal(-128),
290+
false,
291+
},
292+
{
293+
cty.StringVal("FF00"),
294+
cty.NumberIntVal(16),
295+
cty.NumberIntVal(65280),
296+
false,
297+
},
298+
{
299+
cty.StringVal("ff00"),
300+
cty.NumberIntVal(16),
301+
cty.NumberIntVal(65280),
302+
false,
303+
},
304+
{
305+
cty.StringVal("-FF00"),
306+
cty.NumberIntVal(16),
307+
cty.NumberIntVal(-65280),
308+
false,
309+
},
310+
{
311+
cty.StringVal("00FF00"),
312+
cty.NumberIntVal(16),
313+
cty.NumberIntVal(65280),
314+
false,
315+
},
316+
{
317+
cty.StringVal("-00FF00"),
318+
cty.NumberIntVal(16),
319+
cty.NumberIntVal(-65280),
320+
false,
321+
},
322+
{
323+
cty.StringVal("1011111011101111"),
324+
cty.NumberIntVal(2),
325+
cty.NumberIntVal(48879),
326+
false,
327+
},
328+
{
329+
cty.StringVal("aA"),
330+
cty.NumberIntVal(62),
331+
cty.NumberIntVal(656),
332+
false,
333+
},
334+
{
335+
cty.StringVal("Aa"),
336+
cty.NumberIntVal(62),
337+
cty.NumberIntVal(2242),
338+
false,
339+
},
340+
{
341+
cty.StringVal("999999999999999999999999999999999999999999999999999999999999"),
342+
cty.NumberIntVal(10),
343+
cty.MustParseNumberVal("999999999999999999999999999999999999999999999999999999999999"),
344+
false,
345+
},
346+
{
347+
cty.StringVal("FF"),
348+
cty.NumberIntVal(10),
349+
cty.UnknownVal(cty.Number),
350+
true,
351+
},
352+
{
353+
cty.StringVal("00FF"),
354+
cty.NumberIntVal(10),
355+
cty.UnknownVal(cty.Number),
356+
true,
357+
},
358+
{
359+
cty.StringVal("-00FF"),
360+
cty.NumberIntVal(10),
361+
cty.UnknownVal(cty.Number),
362+
true,
363+
},
364+
{
365+
cty.NumberIntVal(2),
366+
cty.NumberIntVal(10),
367+
cty.UnknownVal(cty.Number),
368+
true,
369+
},
370+
{
371+
cty.StringVal("1"),
372+
cty.NumberIntVal(63),
373+
cty.UnknownVal(cty.Number),
374+
true,
375+
},
376+
{
377+
cty.StringVal("1"),
378+
cty.NumberIntVal(-1),
379+
cty.UnknownVal(cty.Number),
380+
true,
381+
},
382+
{
383+
cty.StringVal("1"),
384+
cty.NumberIntVal(1),
385+
cty.UnknownVal(cty.Number),
386+
true,
387+
},
388+
{
389+
cty.StringVal("1"),
390+
cty.NumberIntVal(0),
391+
cty.UnknownVal(cty.Number),
392+
true,
393+
},
394+
{
395+
cty.StringVal("1.2"),
396+
cty.NumberIntVal(10),
397+
cty.UnknownVal(cty.Number),
398+
true,
399+
},
400+
}
401+
402+
for _, test := range tests {
403+
t.Run(fmt.Sprintf("parseint(%#v, %#v)", test.Num, test.Base), func(t *testing.T) {
404+
got, err := ParseInt(test.Num, test.Base)
405+
406+
if test.Err {
407+
if err == nil {
408+
t.Fatal("succeeded; want error")
409+
}
410+
return
411+
} else if err != nil {
412+
t.Fatalf("unexpected error: %s", err)
413+
}
414+
415+
if !got.RawEquals(test.Want) {
416+
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
417+
}
418+
})
419+
}
420+
}

internal/lang/functions.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func (s *Scope) Functions() map[string]function.Function {
8383
"md5": funcs.Md5Func,
8484
"merge": funcs.MergeFunc,
8585
"min": stdlib.MinFunc,
86+
"parseint": funcs.ParseIntFunc,
8687
"pathexpand": funcs.PathExpandFunc,
8788
"pow": funcs.PowFunc,
8889
"range": stdlib.RangeFunc,

internal/lang/functions_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,13 @@ func TestFunctions(t *testing.T) {
513513
},
514514
},
515515

516+
"parseint": {
517+
{
518+
`parseint("100", 10)`,
519+
cty.NumberIntVal(100),
520+
},
521+
},
522+
516523
"pathexpand": {
517524
{
518525
`pathexpand("~/test-file")`,

0 commit comments

Comments
 (0)