diff options
author | raoulb <[email protected]> | 2024-07-29 11:05:36 +0200 |
---|---|---|
committer | GitHub <[email protected]> | 2024-07-29 11:05:36 +0200 |
commit | 9d2b5f98d0efa06d5faf1a7596a53051bc1e088c (patch) | |
tree | 685af5ed561e148f926ca20f9ee40a2b606ed58b /tpl | |
parent | 0e005616204af78c17c402f9b01a41a857861551 (diff) | |
download | hugo-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.go | 70 | ||||
-rw-r--r-- | tpl/math/math.go | 124 | ||||
-rw-r--r-- | tpl/math/math_test.go | 332 |
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) + } +} |