1from ..core import (Dummy, Expr, Float, Integer, PoleError, Rational, Symbol,
2                    nan, oo)
3from ..core.sympify import sympify
4from ..functions.elementary.trigonometric import cos, sin
5from .gruntz import limitinf
6from .order import Order
7
8
9def limit(expr, z, z0, dir='+'):
10    """
11    Compute the directional limit of ``expr`` at the point ``z0``.
12
13    Examples
14    ========
15
16    >>> limit(sin(x)/x, x, 0)
17    1
18    >>> limit(1/x, x, 0, dir='+')
19    oo
20    >>> limit(1/x, x, 0, dir='-')
21    -oo
22    >>> limit(1/x, x, oo)
23    0
24
25    See Also
26    ========
27
28    Limit
29
30    """
31    return Limit(expr, z, z0, dir).doit(deep=False)
32
33
34def heuristics(e, z, z0, dir):
35    rv = None
36
37    if isinstance(e, Expr):
38        e = e.expand()
39
40    if abs(z0) is oo:
41        rv = limit(e.subs({z: 1/z}), z, Integer(0), '+' if z0 is oo else '-')
42        if isinstance(rv, Limit):
43            return
44    elif e.is_Mul or e.is_Add or e.is_Pow or e.is_Function:
45        r = []
46        for a in e.args:
47            l = limit(a, z, z0, dir)
48            if l.has(oo) and (l.func not in (sin, cos) and l.is_finite is None):
49                return
50            elif isinstance(l, Limit):
51                return
52            else:
53                r.append(l)
54        rv = e.func(*r)
55        if rv is nan:
56            return
57
58    return rv
59
60
61class Limit(Expr):
62    r"""Represents a directional limit of ``expr`` at the point ``z0``.
63
64    Parameters
65    ==========
66
67    expr : Expr
68        algebraic expression
69    z    : Symbol
70        variable of the ``expr``
71    z0   : Expr
72        limit point, `z_0`
73    dir  : {"+", "-", "real"}, optional
74        For ``dir="+"`` (default) it calculates the limit from the right
75        (`z\to z_0 + 0`) and for ``dir="-"`` the limit from the left (`z\to
76        z_0 - 0`).  If ``dir="real"``, the limit is the bidirectional real
77        limit.  For infinite ``z0`` (``oo`` or ``-oo``), the ``dir`` argument
78        is determined from the direction of the infinity (i.e.,
79        ``dir="-"`` for ``oo``).
80
81    Examples
82    ========
83
84    >>> Limit(sin(x)/x, x, 0)
85    Limit(sin(x)/x, x, 0)
86    >>> Limit(1/x, x, 0, dir='-')
87    Limit(1/x, x, 0, dir='-')
88
89    """
90
91    def __new__(cls, e, z, z0, dir='+'):
92        e = sympify(e)
93        z = sympify(z)
94        z0 = sympify(z0)
95
96        if z0 is oo:
97            dir = '-'
98        elif z0 == -oo:
99            dir = '+'
100
101        if isinstance(dir, str):
102            dir = Symbol(dir)
103        elif not isinstance(dir, Symbol):
104            raise TypeError(f'direction must be of type str or Symbol, not {type(dir)}')
105        if str(dir) not in ('+', '-', 'real'):
106            raise ValueError(
107                f"direction must be either '+' or '-' or 'real', not {dir}")
108
109        obj = Expr.__new__(cls)
110        obj._args = (e, z, z0, dir)
111        return obj
112
113    @property
114    def free_symbols(self):
115        e, z, z0 = self.args[:3]
116        return (e.free_symbols - z.free_symbols) | z0.free_symbols
117
118    def doit(self, **hints):
119        """Evaluates limit.
120
121        Notes
122        =====
123
124        First we handle some trivial cases (i.e. constant), then try
125        Gruntz algorithm (see the :py:mod:`~diofant.series.gruntz` module).
126
127        """
128        e, z, z0, dir = self.args
129
130        if hints.get('deep', True):
131            e = e.doit(**hints)
132            z = z.doit(**hints)
133            z0 = z0.doit(**hints)
134
135        if str(dir) == 'real':
136            right = limit(e, z, z0, '+')
137            left = limit(e, z, z0, '-')
138            if not left.equals(right):
139                raise PoleError(f'left and right limits for expression {e} at '
140                                f'point {z}={z0} seems to be not equal')
141            else:
142                return right
143
144        use_heuristics = hints.get('heuristics', True)
145
146        has_Floats = e.has(Float) or z0.has(Float)
147        if has_Floats:
148            e = e.subs({k: Rational(k) for k in e.atoms(Float)})
149            z0 = z0.subs({k: Rational(k) for k in z0.atoms(Float)})
150
151        if z0.has(z):
152            newz = z.as_dummy()
153            r = limit(e.subs({z: newz}), newz, z0, dir)
154            if isinstance(r, Limit):
155                r = r.subs({newz: z})
156            return r
157
158        if e == z:
159            return z0
160
161        if not e.has(z):
162            return e
163
164        if z0 is nan:
165            return nan
166
167        if e.is_Relational:
168            ll = limit(e.lhs, z, z0, dir)
169            rl = limit(e.rhs, z, z0, dir)
170
171            if any(isinstance(a, Limit) for a in [ll, rl]):
172                return self
173            else:
174                try:
175                    return e.func(ll, rl)
176                except TypeError:
177                    return self
178
179        if e.has(Order):
180            e = e.expand()
181            order = e.getO()
182            if order:
183                if (z, z0) in zip(order.variables, order.point):
184                    order = limit(order.expr, z, z0, dir)
185                    e = e.removeO() + order
186
187        try:
188            # Convert to the limit z->oo and use Gruntz algorithm.
189            newe, newz = e, z
190            if z0 == -oo:
191                newe = e.subs({z: -z})
192            elif z0 != oo:
193                if str(dir) == '+':
194                    newe = e.subs({z: z0 + 1/z})
195                else:
196                    newe = e.subs({z: z0 - 1/z})
197
198            # We need a fresh variable with correct assumptions.
199            newz = Dummy(z.name, positive=True, finite=True)
200            newe = newe.subs({z: newz})
201
202            r = limitinf(newe, newz)
203        except (PoleError, ValueError, NotImplementedError):
204            r = None
205            if use_heuristics:
206                r = heuristics(e, z, z0, dir)
207            if r is None:
208                return self
209
210        if has_Floats:
211            r = r.evalf()
212
213        return r
214