From 27466faadb83598737354b3550b6a05380f2fafa Mon Sep 17 00:00:00 2001 From: Bnyro Date: Thu, 26 Jun 2025 15:14:16 +0200 Subject: [PATCH] [feat] calculator: add support for comparation operators (<, <=, ==, ...) --- searx/plugins/calculator.py | 65 +++++++++++++++++++++++++++++++++---- 1 file 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)