Repository: mpmath/mpmath (python)

Issue:
mp.tanh and mp.tan return incorrect results at complex infinities
As in the title:
```python
>>> mpmath.mp.tanh(mpmath.mp.mpc(mpmath.inf, mpmath.inf))  # expected (1+0j)
mpc(real='nan', imag='nan')
>>> mpmath.mp.tanh(mpmath.mp.mpc(mpmath.inf, -mpmath.inf))  # expected (1+0j)
mpc(real='nan', imag='nan')
>>> mpmath.mp.tanh(mpmath.mp.mpc(-mpmath.inf, mpmath.inf))  # expected (-1+0j)
mpc(real='nan', imag='nan')
>>> mpmath.mp.tanh(mpmath.mp.mpc(-mpmath.inf, -mpmath.inf))  # expected (-1+0j)
mpc(real='nan', imag='nan')
>>> mpmath.mp.tanh(mpmath.mp.mpc(mpmath.inf, 2))  # expected (1-0j)
mpc(real='nan', imag='0.0')
>>> mpmath.mp.tanh(mpmath.mp.mpc(-mpmath.inf, 2))  # expected (-1-0j)
mpc(real='nan', imag='0.0')
>>> mpmath.mp.tanh(mpmath.mp.mpc(0, -mpmath.inf))  # expected (nan+nanj)
mpc(real='0.0', imag='nan')
```
mpmath.fp.tanh returns expected results (except for the last sample).

```python
>>> mpmath.mp.tan(mpmath.mp.mpc(mpmath.inf, mpmath.inf))  # expected (1j)
mpc(real='nan', imag='nan')
>>> mpmath.mp.tan(mpmath.mp.mpc(mpmath.inf, -mpmath.inf))  # expected (-1j)
mpc(real='nan', imag='nan')
>>> mpmath.mp.tan(mpmath.mp.mpc(-mpmath.inf, mpmath.inf))  # expected (1j)
mpc(real='nan', imag='nan')
>>> mpmath.mp.tan(mpmath.mp.mpc(-mpmath.inf, -mpmath.inf))  # expected (-1j)
mpc(real='nan', imag='nan')
>>> mpmath.mp.tan(mpmath.mp.mpc(2, mpmath.inf))  # expected (-0+1j)
mpc(real='0.0', imag='nan')
>>> mpmath.mp.tan(mpmath.mp.mpc(2, -mpmath.inf))  # expected (-0-1j)
mpc(real='0.0', imag='nan')
>>> mpmath.mp.tan(mpmath.mp.mpc(-mpmath.inf, 0))  # expected (nan+nanj)
mpc(real='nan', imag='0.0')
```
mpmath.fp.tan returns expected results (except for the last sample).

Expected values are defined in https://en.cppreference.com/w/cpp/numeric/complex/tanh and https://en.cppreference.com/w/cpp/numeric/complex/tan.

Pull request:
Handle infinite arguments in tan/tanh
See https://en.cppreference.com/w/c/numeric/complex/ctanh and https://en.cppreference.com/w/c/numeric/complex/ctan

Closes #781

Patch:
diff --git a/mpmath/libfp.py b/mpmath/libfp.py
index c6d5666..ebd596b 100644
--- a/mpmath/libfp.py
+++ b/mpmath/libfp.py
@@ -47,6 +47,23 @@ def f(*args, **kwargs):
     return f
 
 
+def _tan_complex(z):
+    if math.isinf(z.real):
+        if z.imag == 0:
+            return cmath.nan + 0j
+        elif math.isfinite(z.imag):
+            return cmath.nan + cmath.nanj
+    return cmath.tan(z)
+
+def _tanh_complex(z):
+    if math.isinf(z.imag):
+        if z.real == 0:
+            return cmath.nanj
+        elif math.isfinite(z.real):
+            return cmath.nan + cmath.nanj
+    return cmath.tanh(z)
+
+
 pow = _mathfun_n(operator.pow, lambda x, y: complex(x)**y)
 log = _mathfun_n(math.log, cmath.log)
 sqrt = _mathfun(math.sqrt, cmath.sqrt)
@@ -54,7 +71,7 @@ def f(*args, **kwargs):
 
 cos = _mathfun_real(math.cos, cmath.cos)
 sin = _mathfun_real(math.sin, cmath.sin)
-tan = _mathfun_real(math.tan, cmath.tan)
+tan = _mathfun_real(math.tan, _tan_complex)
 
 acos = _mathfun(math.acos, cmath.acos)
 asin = _mathfun(math.asin, cmath.asin)
@@ -62,7 +79,7 @@ def f(*args, **kwargs):
 
 cosh = _mathfun_real(math.cosh, cmath.cosh)
 sinh = _mathfun_real(math.sinh, cmath.sinh)
-tanh = _mathfun_real(math.tanh, cmath.tanh)
+tanh = _mathfun_real(math.tanh, _tanh_complex)
 
 acosh = _mathfun(math.acosh, cmath.acosh)
 asinh = _mathfun(math.asinh, cmath.asinh)
diff --git a/mpmath/libmp/libmpc.py b/mpmath/libmp/libmpc.py
index 32eb87d..b125b60 100644
--- a/mpmath/libmp/libmpc.py
+++ b/mpmath/libmp/libmpc.py
@@ -12,13 +12,13 @@
                         mpf_pow_int, mpf_sin, mpf_sin_pi, mpf_sinh, mpf_tan,
                         mpf_tanh)
 from .libintmath import giant_steps, lshift, rshift
-from .libmpf import (ComplexResult, fhalf, finf, fnan, fninf, fnone, fone,
-                     from_float, from_int, from_man_exp, ftwo, fzero, mpf_abs,
-                     mpf_add, mpf_ceil, mpf_div, mpf_floor, mpf_frac, mpf_hash,
-                     mpf_hypot, mpf_mul, mpf_mul_int, mpf_neg, mpf_nint,
-                     mpf_pos, mpf_rdiv_int, mpf_shift, mpf_sqrt, mpf_sub,
-                     normalize, reciprocal_rnd, round_fast, round_floor,
-                     to_fixed, to_float, to_int, to_str)
+from .libmpf import (ComplexResult, fhalf, finf, fnan, fninf, fnone, fnzero,
+                     fone, from_float, from_int, from_man_exp, ftwo, fzero,
+                     mpf_abs, mpf_add, mpf_ceil, mpf_div, mpf_floor, mpf_frac,
+                     mpf_hash, mpf_hypot, mpf_mul, mpf_mul_int, mpf_neg,
+                     mpf_nint, mpf_pos, mpf_rdiv_int, mpf_shift, mpf_sqrt,
+                     mpf_sub, normalize, reciprocal_rnd, round_fast,
+                     round_floor, to_fixed, to_float, to_int, to_str)
 
 
 # An mpc value is a (real, imag) tuple
@@ -474,6 +474,14 @@ def mpc_tan(z, prec, rnd=round_fast):
     """Complex tangent. Computed as tan(a+bi) = sin(2a)/M + sinh(2b)/M*i
     where M = cos(2a) + cosh(2b)."""
     a, b = z
+    if b == finf:
+        return fzero, fone
+    if b == fninf:
+        return fzero, fnone
+    if a in (finf, fninf):
+        if b in (fzero, fnzero):
+            return fnan, fzero
+        return fnan, fnan
     asign, aman, aexp, abc = a
     bsign, bman, bexp, bbc = b
     if b == fzero: return mpf_tan(a, prec, rnd), fzero


Test patch:
diff --git a/mpmath/tests/test_fp.py b/mpmath/tests/test_fp.py
index 6a21123..b37e32a 100644
--- a/mpmath/tests/test_fp.py
+++ b/mpmath/tests/test_fp.py
@@ -117,6 +117,34 @@ def test_fp_asinh_acosh_atanh():
     assert ae(fp.acosh(1), 0.0)
     assert ae(fp.atanh(0), 0.0)
 
+def test_fp_tanh():
+    assert fp.tanh(fp.mpc(+fp.inf, +fp.inf)) == fp.mpc(+1, 0)
+    assert fp.tanh(fp.mpc(+fp.inf, -fp.inf)) == fp.mpc(+1, 0)
+    assert fp.tanh(fp.mpc(-fp.inf, +fp.inf)) == fp.mpc(-1, 0)
+    assert fp.tanh(fp.mpc(-fp.inf, -fp.inf)) == fp.mpc(-1, 0)
+    assert fp.tanh(fp.mpc(+fp.inf, 2)) == fp.mpc(+1, 0)
+    assert fp.tanh(fp.mpc(-fp.inf, 2)) == fp.mpc(-1, 0)
+    r = fp.tanh(fp.mpc(0, -fp.inf))
+    assert r.real == 0 and fp.isnan(r.imag)
+    r = fp.tanh(fp.mpc(2, -fp.inf))
+    assert fp.isnan(r.real) and fp.isnan(r.imag)
+    assert fp.tanh(fp.mpc(+fp.inf, fp.nan)) == fp.mpc(+1, 0)
+    assert fp.tanh(fp.mpc(-fp.inf, fp.nan)) == fp.mpc(-1, 0)
+
+def test_fp_tan():
+    assert fp.tan(fp.mpc(+fp.inf, +fp.inf)) == fp.mpc(0, +1)
+    assert fp.tan(fp.mpc(+fp.inf, -fp.inf)) == fp.mpc(0, -1)
+    assert fp.tan(fp.mpc(-fp.inf, +fp.inf)) == fp.mpc(0, +1)
+    assert fp.tan(fp.mpc(-fp.inf, -fp.inf)) == fp.mpc(0, -1)
+    assert fp.tan(fp.mpc(2, +fp.inf)) == fp.mpc(0, +1)
+    assert fp.tan(fp.mpc(2, -fp.inf)) == fp.mpc(0, -1)
+    r = fp.tan(fp.mpc(-fp.inf, 0))
+    assert fp.isnan(r.real) and r.imag == 0
+    r = fp.tan(fp.mpc(-fp.inf, 2))
+    assert fp.isnan(r.real) and fp.isnan(r.imag)
+    assert fp.tan(fp.mpc(fp.nan, +fp.inf)) == fp.mpc(0, +1)
+    assert fp.tan(fp.mpc(fp.nan, -fp.inf)) == fp.mpc(0, -1)
+
 def test_fp_expj():
     assert ae(fp.expj(0), (1.0 + 0.0j))
     assert ae(fp.expj(1), (0.5403023058681397174 + 0.84147098480789650665j))
diff --git a/mpmath/tests/test_functions.py b/mpmath/tests/test_functions.py
index de4b708..908461f 100644
--- a/mpmath/tests/test_functions.py
+++ b/mpmath/tests/test_functions.py
@@ -841,6 +841,33 @@ def test_tanh():
     assert isnan(tanh(nan))
     assert tanh(mpc('inf', '0')) == 1
 
+    assert tanh(mpc(+inf, +inf)) == mpc(+1, 0)
+    assert tanh(mpc(+inf, -inf)) == mpc(+1, 0)
+    assert tanh(mpc(-inf, +inf)) == mpc(-1, 0)
+    assert tanh(mpc(-inf, -inf)) == mpc(-1, 0)
+    assert tanh(mpc(+inf, 2)) == mpc(+1, 0)
+    assert tanh(mpc(-inf, 2)) == mpc(-1, 0)
+    r = tanh(mpc(0, -inf))
+    assert r.real == 0 and isnan(r.imag)
+    r = tanh(mpc(2, -inf))
+    assert isnan(r.real) and isnan(r.imag)
+    assert tanh(mpc(+inf, nan)) == mpc(+1, 0)
+    assert tanh(mpc(-inf, nan)) == mpc(-1, 0)
+
+def test_tan():
+    assert tan(mpc(+inf, +inf)) == mpc(0, +1)
+    assert tan(mpc(+inf, -inf)) == mpc(0, -1)
+    assert tan(mpc(-inf, +inf)) == mpc(0, +1)
+    assert tan(mpc(-inf, -inf)) == mpc(0, -1)
+    assert tan(mpc(2, +inf)) == mpc(0, +1)
+    assert tan(mpc(2, -inf)) == mpc(0, -1)
+    r = tan(mpc(-inf, 0))
+    assert isnan(r.real) and r.imag == 0
+    r = tan(mpc(-inf, 2))
+    assert isnan(r.real) and isnan(r.imag)
+    assert tan(mpc(nan, +inf)) == mpc(0, +1)
+    assert tan(mpc(nan, -inf)) == mpc(0, -1)
+
 def test_atanh():
     assert atanh(0) == 0
     assert atanh(0.5).ae(0.54930614433405484570)