diff --git a/tpl/math/math.go b/tpl/math/math.go index 5ddc3eb9c..b79cb6188 100644 --- a/tpl/math/math.go +++ b/tpl/math/math.go @@ -34,11 +34,31 @@ func (ns *Namespace) Add(a, b interface{}) (interface{}, error) { return DoArithmetic(a, b, '+') } +// Ceil returns the least integer value greater than or equal to x. +func (ns *Namespace) Ceil(x interface{}) (float64, error) { + xf, err := cast.ToFloat64E(x) + if err != nil { + return 0, errors.New("Ceil operator can't be used with non-float value") + } + + return math.Ceil(xf), nil +} + // Div divides two numbers. func (ns *Namespace) Div(a, b interface{}) (interface{}, error) { return DoArithmetic(a, b, '/') } +// Floor returns the greatest integer value less than or equal to x. +func (ns *Namespace) Floor(x interface{}) (float64, error) { + xf, err := cast.ToFloat64E(x) + if err != nil { + return 0, errors.New("Floor operator can't be used with non-float value") + } + + return math.Floor(xf), nil +} + // Log returns the natural logarithm of a number. func (ns *Namespace) Log(a interface{}) (float64, error) { af, err := cast.ToFloat64E(a) @@ -92,6 +112,16 @@ func (ns *Namespace) Mul(a, b interface{}) (interface{}, error) { return DoArithmetic(a, b, '*') } +// Round returns the nearest integer, rounding half away from zero. +func (ns *Namespace) Round(x interface{}) (float64, error) { + xf, err := cast.ToFloat64E(x) + if err != nil { + return 0, errors.New("Round operator can't be used with non-float value") + } + + return _round(xf), nil +} + // Sub subtracts two numbers. func (ns *Namespace) Sub(a, b interface{}) (interface{}, error) { return DoArithmetic(a, b, '-') diff --git a/tpl/math/math_test.go b/tpl/math/math_test.go index 49ac053fd..4d14a58cc 100644 --- a/tpl/math/math_test.go +++ b/tpl/math/math_test.go @@ -143,6 +143,72 @@ func TestDoArithmetic(t *testing.T) { } } +func TestCeil(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + x interface{} + expect interface{} + }{ + {0.1, 1.0}, + {0.5, 1.0}, + {1.1, 2.0}, + {1.5, 2.0}, + {-0.1, 0.0}, + {-0.5, 0.0}, + {-1.1, -1.0}, + {-1.5, -1.0}, + {"abc", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Ceil(test.x) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestFloor(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + x interface{} + expect interface{} + }{ + {0.1, 0.0}, + {0.5, 0.0}, + {1.1, 1.0}, + {1.5, 1.0}, + {-0.1, -1.0}, + {-0.5, -1.0}, + {-1.1, -2.0}, + {-1.5, -2.0}, + {"abc", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Floor(test.x) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + func TestLog(t *testing.T) { t.Parallel() @@ -255,3 +321,36 @@ func TestModBool(t *testing.T) { assert.Equal(t, test.expect, result, errMsg) } } + +func TestRound(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + x interface{} + expect interface{} + }{ + {0.1, 0.0}, + {0.5, 1.0}, + {1.1, 1.0}, + {1.5, 2.0}, + {-0.1, -0.0}, + {-0.5, -1.0}, + {-1.1, -1.0}, + {-1.5, -2.0}, + {"abc", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Round(test.x) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/math/round.go b/tpl/math/round.go new file mode 100644 index 000000000..9b33120af --- /dev/null +++ b/tpl/math/round.go @@ -0,0 +1,61 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// According to https://github.com/golang/go/issues/20100, the Go stdlib will +// include math.Round beginning with Go 1.10. +// +// The following implementation was taken from https://golang.org/cl/43652. + +package math + +import "math" + +const ( + mask = 0x7FF + shift = 64 - 11 - 1 + bias = 1023 +) + +// Round returns the nearest integer, rounding half away from zero. +// +// Special cases are: +// Round(±0) = ±0 +// Round(±Inf) = ±Inf +// Round(NaN) = NaN +func _round(x float64) float64 { + // Round is a faster implementation of: + // + // func Round(x float64) float64 { + // t := Trunc(x) + // if Abs(x-t) >= 0.5 { + // return t + Copysign(1, x) + // } + // return t + // } + const ( + signMask = 1 << 63 + fracMask = 1<>shift) & mask + if e < bias { + // Round abs(x) < 1 including denormals. + bits &= signMask // +-0 + if e == bias-1 { + bits |= one // +-1 + } + } else if e < bias+shift { + // Round any abs(x) >= 1 containing a fractional component [0,1). + // + // Numbers with larger exponents are returned unchanged since they + // must be either an integer, infinity, or NaN. + e -= bias + bits += half >> e + bits &^= fracMask >> e + } + return math.Float64frombits(bits) +}