aboutsummaryrefslogtreecommitdiffhomepage
path: root/tpl
diff options
context:
space:
mode:
authorraoulb <[email protected]>2024-07-29 11:05:36 +0200
committerGitHub <[email protected]>2024-07-29 11:05:36 +0200
commit9d2b5f98d0efa06d5faf1a7596a53051bc1e088c (patch)
tree685af5ed561e148f926ca20f9ee40a2b606ed58b /tpl
parent0e005616204af78c17c402f9b01a41a857861551 (diff)
downloadhugo-9d2b5f98d0efa06d5faf1a7596a53051bc1e088c.tar.gz
hugo-9d2b5f98d0efa06d5faf1a7596a53051bc1e088c.zip
math: Add trigonometric functions and some angle helper functions
This commit adds these new template functions in the `math` namespace: math.Acos math.Asin math.Atan math.Atan2 math.Cos math.Pi math.Sin math.Tan math.ToDegrees math.ToRadians Co-authored-by: Joe Mooring <[email protected]>
Diffstat (limited to 'tpl')
-rw-r--r--tpl/math/init.go70
-rw-r--r--tpl/math/math.go124
-rw-r--r--tpl/math/math_test.go332
3 files changed, 510 insertions, 16 deletions
diff --git a/tpl/math/init.go b/tpl/math/init.go
index fa1367671..0ef02550c 100644
--- a/tpl/math/init.go
+++ b/tpl/math/init.go
@@ -38,6 +38,13 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.Acos,
+ nil,
+ [][2]string{
+ {"{{ math.Acos 1 }}", "0"},
+ },
+ )
+
ns.AddMethodMapping(ctx.Add,
[]string{"add"},
[][2]string{
@@ -45,6 +52,27 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.Asin,
+ nil,
+ [][2]string{
+ {"{{ math.Asin 1 }}", "1.5707963267948966"},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.Atan,
+ nil,
+ [][2]string{
+ {"{{ math.Atan 1 }}", "0.7853981633974483"},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.Atan2,
+ nil,
+ [][2]string{
+ {"{{ math.Atan2 1 2 }}", "0.4636476090008061"},
+ },
+ )
+
ns.AddMethodMapping(ctx.Ceil,
nil,
[][2]string{
@@ -52,6 +80,13 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.Cos,
+ nil,
+ [][2]string{
+ {"{{ math.Cos 1 }}", "0.5403023058681398"},
+ },
+ )
+
ns.AddMethodMapping(ctx.Div,
[]string{"div"},
[][2]string{
@@ -108,6 +143,13 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.Pi,
+ nil,
+ [][2]string{
+ {"{{ math.Pi }}", "3.141592653589793"},
+ },
+ )
+
ns.AddMethodMapping(ctx.Pow,
[]string{"pow"},
[][2]string{
@@ -129,6 +171,13 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.Sin,
+ nil,
+ [][2]string{
+ {"{{ math.Sin 1 }}", "0.8414709848078965"},
+ },
+ )
+
ns.AddMethodMapping(ctx.Sqrt,
nil,
[][2]string{
@@ -143,6 +192,27 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.Tan,
+ nil,
+ [][2]string{
+ {"{{ math.Tan 1 }}", "1.557407724654902"},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.ToDegrees,
+ nil,
+ [][2]string{
+ {"{{ math.ToDegrees 1.5707963267948966 }}", "90"},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.ToRadians,
+ nil,
+ [][2]string{
+ {"{{ math.ToRadians 90 }}", "1.5707963267948966"},
+ },
+ )
+
return ns
}
diff --git a/tpl/math/math.go b/tpl/math/math.go
index 3368f6b73..15c0db22c 100644
--- a/tpl/math/math.go
+++ b/tpl/math/math.go
@@ -49,11 +49,51 @@ func (ns *Namespace) Abs(n any) (float64, error) {
return math.Abs(af), nil
}
+// Acos returns the arccosine, in radians, of n.
+func (ns *Namespace) Acos(n any) (float64, error) {
+ af, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires a numeric argument")
+ }
+ return math.Acos(af), nil
+}
+
// Add adds the multivalued addends n1 and n2 or more values.
func (ns *Namespace) Add(inputs ...any) (any, error) {
return ns.doArithmetic(inputs, '+')
}
+// Asin returns the arcsine, in radians, of n.
+func (ns *Namespace) Asin(n any) (float64, error) {
+ af, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires a numeric argument")
+ }
+ return math.Asin(af), nil
+}
+
+// Atan returns the arctangent, in radians, of n.
+func (ns *Namespace) Atan(n any) (float64, error) {
+ af, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires a numeric argument")
+ }
+ return math.Atan(af), nil
+}
+
+// Atan2 returns the arc tangent of n/m, using the signs of the two to determine the quadrant of the return value.
+func (ns *Namespace) Atan2(n, m any) (float64, error) {
+ afx, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires numeric arguments")
+ }
+ afy, err := cast.ToFloat64E(m)
+ if err != nil {
+ return 0, errors.New("requires numeric arguments")
+ }
+ return math.Atan2(afx, afy), nil
+}
+
// Ceil returns the least integer value greater than or equal to n.
func (ns *Namespace) Ceil(n any) (float64, error) {
xf, err := cast.ToFloat64E(n)
@@ -64,6 +104,15 @@ func (ns *Namespace) Ceil(n any) (float64, error) {
return math.Ceil(xf), nil
}
+// Cos returns the cosine of the radian argument n.
+func (ns *Namespace) Cos(n any) (float64, error) {
+ af, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires a numeric argument")
+ }
+ return math.Cos(af), nil
+}
+
// Div divides n1 by n2.
func (ns *Namespace) Div(inputs ...any) (any, error) {
return ns.doArithmetic(inputs, '/')
@@ -99,22 +148,6 @@ func (ns *Namespace) Min(inputs ...any) (minimum float64, err error) {
return ns.applyOpToScalarsOrSlices("Min", math.Min, inputs...)
}
-// Sum returns the sum of all numbers in inputs. Any slices in inputs are flattened.
-func (ns *Namespace) Sum(inputs ...any) (sum float64, err error) {
- fn := func(x, y float64) float64 {
- return x + y
- }
- return ns.applyOpToScalarsOrSlices("Sum", fn, inputs...)
-}
-
-// Product returns the product of all numbers in inputs. Any slices in inputs are flattened.
-func (ns *Namespace) Product(inputs ...any) (product float64, err error) {
- fn := func(x, y float64) float64 {
- return x * y
- }
- return ns.applyOpToScalarsOrSlices("Product", fn, inputs...)
-}
-
// Mod returns n1 % n2.
func (ns *Namespace) Mod(n1, n2 any) (int64, error) {
ai, erra := cast.ToInt64E(n1)
@@ -146,6 +179,11 @@ func (ns *Namespace) Mul(inputs ...any) (any, error) {
return ns.doArithmetic(inputs, '*')
}
+// Pi returns the mathematical constant pi.
+func (ns *Namespace) Pi() float64 {
+ return math.Pi
+}
+
// Pow returns n1 raised to the power of n2.
func (ns *Namespace) Pow(n1, n2 any) (float64, error) {
af, erra := cast.ToFloat64E(n1)
@@ -158,6 +196,14 @@ func (ns *Namespace) Pow(n1, n2 any) (float64, error) {
return math.Pow(af, bf), nil
}
+// Product returns the product of all numbers in inputs. Any slices in inputs are flattened.
+func (ns *Namespace) Product(inputs ...any) (product float64, err error) {
+ fn := func(x, y float64) float64 {
+ return x * y
+ }
+ return ns.applyOpToScalarsOrSlices("Product", fn, inputs...)
+}
+
// Rand returns, as a float64, a pseudo-random number in the half-open interval [0.0,1.0).
func (ns *Namespace) Rand() float64 {
return rand.Float64()
@@ -173,6 +219,15 @@ func (ns *Namespace) Round(n any) (float64, error) {
return _round(xf), nil
}
+// Sin returns the sine of the radian argument n.
+func (ns *Namespace) Sin(n any) (float64, error) {
+ af, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires a numeric argument")
+ }
+ return math.Sin(af), nil
+}
+
// Sqrt returns the square root of the number n.
func (ns *Namespace) Sqrt(n any) (float64, error) {
af, err := cast.ToFloat64E(n)
@@ -188,6 +243,43 @@ func (ns *Namespace) Sub(inputs ...any) (any, error) {
return ns.doArithmetic(inputs, '-')
}
+// Sum returns the sum of all numbers in inputs. Any slices in inputs are flattened.
+func (ns *Namespace) Sum(inputs ...any) (sum float64, err error) {
+ fn := func(x, y float64) float64 {
+ return x + y
+ }
+ return ns.applyOpToScalarsOrSlices("Sum", fn, inputs...)
+}
+
+// Tan returns the tangent of the radian argument n.
+func (ns *Namespace) Tan(n any) (float64, error) {
+ af, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires a numeric argument")
+ }
+ return math.Tan(af), nil
+}
+
+// ToDegrees converts radians into degrees.
+func (ns *Namespace) ToDegrees(n any) (float64, error) {
+ af, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires a numeric argument")
+ }
+
+ return af * 180 / math.Pi, nil
+}
+
+// ToRadians converts degrees into radians.
+func (ns *Namespace) ToRadians(n any) (float64, error) {
+ af, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires a numeric argument")
+ }
+
+ return af * math.Pi / 180, nil
+}
+
func (ns *Namespace) applyOpToScalarsOrSlices(opName string, op func(x, y float64) float64, inputs ...any) (result float64, err error) {
var i int
var hasValue bool
diff --git a/tpl/math/math_test.go b/tpl/math/math_test.go
index 4cde3fb85..67abbc27e 100644
--- a/tpl/math/math_test.go
+++ b/tpl/math/math_test.go
@@ -547,3 +547,335 @@ func TestProduct(t *testing.T) {
_, err := ns.Product()
c.Assert(err, qt.Not(qt.IsNil))
}
+
+// Test trigonometric functions
+
+func TestPi(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+
+ ns := New()
+
+ expect := 3.1415
+ result := ns.Pi()
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(result, qt.Equals, expect)
+}
+
+func TestSin(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+
+ ns := New()
+
+ for _, test := range []struct {
+ a any
+ expect any
+ }{
+ {0, 0.0},
+ {1, 0.8414},
+ {math.Pi / 2, 1.0},
+ {math.Pi, 0.0},
+ {-1.0, -0.8414},
+ {"abc", false},
+ } {
+
+ result, err := ns.Sin(test.a)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+}
+
+func TestCos(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+
+ ns := New()
+
+ for _, test := range []struct {
+ a any
+ expect any
+ }{
+ {0, 1.0},
+ {1, 0.5403},
+ {math.Pi / 2, 0.0},
+ {math.Pi, -1.0},
+ {-1.0, 0.5403},
+ {"abc", false},
+ } {
+
+ result, err := ns.Cos(test.a)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+}
+
+func TestTan(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+
+ ns := New()
+
+ for _, test := range []struct {
+ a any
+ expect any
+ }{
+ {0, 0.0},
+ {1, 1.5574},
+ // {math.Pi / 2, math.Inf(1)},
+ {math.Pi, 0.0},
+ {-1.0, -1.5574},
+ {"abc", false},
+ } {
+
+ result, err := ns.Tan(test.a)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ if result != math.Inf(1) {
+ result = float64(int(result*10000)) / 10000
+ }
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+
+ // Separate test for Tan(oo) -- returns NaN
+ result, err := ns.Tan(math.Inf(1))
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Satisfies, math.IsNaN)
+}
+
+// Test inverse trigonometric functions
+
+func TestAsin(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+ ns := New()
+
+ for _, test := range []struct {
+ x any
+ expect any
+ }{
+ {0.0, 0.0},
+ {1.0, 1.5707},
+ {-1.0, -1.5707},
+ {0.5, 0.5235},
+ {"abc", false},
+ } {
+ result, err := ns.Asin(test.x)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+
+ // Separate test for Asin(2) -- returns NaN
+ result, err := ns.Asin(2)
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Satisfies, math.IsNaN)
+}
+
+func TestAcos(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+ ns := New()
+
+ for _, test := range []struct {
+ x any
+ expect any
+ }{
+ {1.0, 0.0},
+ {0.0, 1.5707},
+ {-1.0, 3.1415},
+ {0.5, 1.0471},
+ {"abc", false},
+ } {
+ result, err := ns.Acos(test.x)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+
+ // Separate test for Acos(2) -- returns NaN
+ result, err := ns.Acos(2)
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Satisfies, math.IsNaN)
+}
+
+func TestAtan(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+ ns := New()
+
+ for _, test := range []struct {
+ x any
+ expect any
+ }{
+ {0.0, 0.0},
+ {1, 0.7853},
+ {-1.0, -0.7853},
+ {math.Inf(1), 1.5707},
+ {"abc", false},
+ } {
+ result, err := ns.Atan(test.x)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+}
+
+func TestAtan2(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+ ns := New()
+
+ for _, test := range []struct {
+ x any
+ y any
+ expect any
+ }{
+ {1.0, 1.0, 0.7853},
+ {-1.0, 1.0, -0.7853},
+ {1.0, -1.0, 2.3561},
+ {-1.0, -1.0, -2.3561},
+ {1, 0, 1.5707},
+ {-1, 0, -1.5707},
+ {0, 1, 0.0},
+ {0, -1, 3.1415},
+ {0.0, 0.0, 0.0},
+ {"abc", "def", false},
+ } {
+ result, err := ns.Atan2(test.x, test.y)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+}
+
+// Test angle helper functions
+
+func TestToDegrees(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+ ns := New()
+
+ for _, test := range []struct {
+ x any
+ expect any
+ }{
+ {0.0, 0.0},
+ {1, 57.2957},
+ {math.Pi / 2, 90.0},
+ {math.Pi, 180.0},
+ {"abc", false},
+ } {
+ result, err := ns.ToDegrees(test.x)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+}
+
+func TestToRadians(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+ ns := New()
+
+ for _, test := range []struct {
+ x any
+ expect any
+ }{
+ {0, 0.0},
+ {57.29577951308232, 1.0},
+ {90, 1.5707},
+ {180.0, 3.1415},
+ {"abc", false},
+ } {
+ result, err := ns.ToRadians(test.x)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+}