diff --git a/tpl/template.go b/tpl/template.go index 1dc19eb1d..89c522a4f 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -210,6 +210,69 @@ func Slicestr(a interface{}, start, end int) (string, error) { } +// Substr extracts parts of a string, beginning at the character at the specified +// position, and returns the specified number of characters. +// +// It normally takes two parameters: start and length. +// It can also take one parameter: start, i.e. length is omitted, in which case +// the substring starting from start until the end of the string will be returned. +// +// To extract characters from the end of the string, use a negative start number. +// +// In addition, borrowing from the extended behavior described at http://php.net/substr, +// if length is given and is negative, then that many characters will be omitted from +// the end of string. +func Substr(a interface{}, nums ...int) (string, error) { + aStr, err := cast.ToStringE(a) + if err != nil { + return "", err + } + + var start, length int + switch len(nums) { + case 1: + start = nums[0] + length = len(aStr) + case 2: + start = nums[0] + length = nums[1] + default: + return "", errors.New("too many arguments") + } + + if start < -len(aStr) { + start = 0 + } + if start > len(aStr) { + return "", errors.New(fmt.Sprintf("start position out of bounds for %d-byte string", len(aStr))) + } + + var s, e int + if start >= 0 && length >= 0 { + s = start + e = start + length + } else if start < 0 && length >= 0 { + s = len(aStr) + start - length + 1 + e = len(aStr) + start + 1 + } else if start >= 0 && length < 0 { + s = start + e = len(aStr) + length + } else { + s = len(aStr) + start + e = len(aStr) + length + } + + if s > e { + return "", errors.New(fmt.Sprintf("calculated start position greater than end position: %d > %d", s, e)) + } + if e > len(aStr) { + e = len(aStr) + } + + return aStr[s:e], nil + +} + func Split(a interface{}, delimiter string) ([]string, error) { aStr, err := cast.ToStringE(a) if err != nil { @@ -1339,6 +1402,7 @@ func init() { "le": Le, "in": In, "slicestr": Slicestr, + "substr": Substr, "split": Split, "intersect": Intersect, "isSet": IsSet, diff --git a/tpl/template_test.go b/tpl/template_test.go index 3ea7af978..b3764e737 100644 --- a/tpl/template_test.go +++ b/tpl/template_test.go @@ -310,6 +310,46 @@ func TestSlicestr(t *testing.T) { } } +func TestSubstr(t *testing.T) { + for i, this := range []struct { + v1 interface{} + v2 int + v3 int + expect interface{} + }{ + {"abc", 1, 2, "bc"}, + {"abc", 0, 1, "a"}, + {"abcdef", -1, 2, "ef"}, + {"abcdef", -3, 3, "bcd"}, + {"abcdef", 0, -1, "abcde"}, + {"abcdef", 2, -1, "cde"}, + {"abcdef", 4, -4, false}, + {"abcdef", 7, 1, false}, + {"abcdef", 1, 100, "bcdef"}, + {"abcdef", -100, 3, "abc"}, + {"abcdef", -3, -1, "de"}, + {123, 1, 3, "23"}, + {1.2e3, 0, 4, "1200"}, + {tstNoStringer{}, 0, 1, false}, + } { + result, err := Substr(this.v1, this.v2, this.v3) + + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] Substr didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(result, this.expect) { + t.Errorf("[%d] Got %s but expected %s", i, result, this.expect) + } + } + } +} + func TestSplit(t *testing.T) { for i, this := range []struct { v1 interface{}