summaryrefslogtreecommitdiff
path: root/searx/plugins/calculator.py
diff options
context:
space:
mode:
Diffstat (limited to 'searx/plugins/calculator.py')
-rw-r--r--searx/plugins/calculator.py65
1 files changed, 58 insertions, 7 deletions
diff --git a/searx/plugins/calculator.py b/searx/plugins/calculator.py
index 65866ea48..30c414771 100644
--- a/searx/plugins/calculator.py
+++ b/searx/plugins/calculator.py
@@ -76,15 +76,50 @@ class SXNGPlugin(Plugin):
# Prevent the runtime from being longer than 50 ms
res = timeout_func(0.05, _eval_expr, query_py_formatted)
- if res is None or res == "":
+ if res is None or res[0] == "":
return results
- res = babel.numbers.format_decimal(res, locale=ui_locale)
+ res, is_boolean = res
+ if is_boolean:
+ res = "True" if res != 0 else "False"
+ else:
+ res = babel.numbers.format_decimal(res, locale=ui_locale)
results.add(results.types.Answer(answer=f"{search.search_query.query} = {res}"))
return results
+def _compare(ops: list[ast.cmpop], values: list[int | float]) -> int:
+ """
+ 2 < 3 becomes ops=[ast.Lt] and values=[2,3]
+ 2 < 3 <= 4 becomes ops=[ast.Lt, ast.LtE] and values=[2,3, 4]
+ """
+ for op, a, b in zip(ops, values, values[1:]): # pylint: disable=invalid-name
+ if isinstance(op, ast.Eq) and a == b:
+ continue
+ if isinstance(op, ast.NotEq) and a != b:
+ continue
+ if isinstance(op, ast.Lt) and a < b:
+ continue
+ if isinstance(op, ast.LtE) and a <= b:
+ continue
+ if isinstance(op, ast.Gt) and a > b:
+ continue
+ if isinstance(op, ast.GtE) and a >= b:
+ continue
+
+ # Ignore impossible ops:
+ # * ast.Is
+ # * ast.IsNot
+ # * ast.In
+ # * ast.NotIn
+
+ # the result is False for a and b and operation op
+ return 0
+ # the results for all the ops are True
+ return 1
+
+
operators: dict[type, typing.Callable] = {
ast.Add: operator.add,
ast.Sub: operator.sub,
@@ -98,6 +133,7 @@ operators: dict[type, typing.Callable] = {
ast.RShift: operator.rshift,
ast.LShift: operator.lshift,
ast.Mod: operator.mod,
+ ast.Compare: _compare,
}
# with multiprocessing.get_context("fork") we are ready for Py3.14 (by emulating
@@ -109,18 +145,30 @@ mp_fork = multiprocessing.get_context("fork")
def _eval_expr(expr):
"""
+ Evaluates the given textual expression.
+
+ Returns a tuple of (numericResult, isBooleanResult).
+
>>> _eval_expr('2^6')
- 64
+ 64, False
>>> _eval_expr('2**6')
- 64
+ 64, False
>>> _eval_expr('1 + 2*3**(4^5) / (6 + -7)')
- -5.0
+ -5.0, False
+ >>> _eval_expr('1 < 3')
+ 1, True
+ >>> _eval_expr('5 < 3')
+ 0, True
+ >>> _eval_expr('17 == 11+1+5 == 7+5+5')
+ 1, True
"""
try:
- return _eval(ast.parse(expr, mode='eval').body)
+ root_expr = ast.parse(expr, mode='eval').body
+ return _eval(root_expr), isinstance(root_expr, ast.Compare)
+
except ZeroDivisionError:
# This is undefined
- return ""
+ return "", False
def _eval(node):
@@ -133,6 +181,9 @@ def _eval(node):
if isinstance(node, ast.UnaryOp):
return operators[type(node.op)](_eval(node.operand))
+ if isinstance(node, ast.Compare):
+ return _compare(node.ops, [_eval(node.left)] + [_eval(c) for c in node.comparators])
+
raise TypeError(node)