From e495af3bcc4c65e1b982fa328ac21d5733973b35 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Tue, 21 Jul 2026 15:22:08 +0100 Subject: [PATCH 1/4] Added support for bracket notation conversion and corresponding tests in preview --- app/preview_test.py | 22 ++++++++++++++++++++++ app/utility/expression_utilities.py | 10 ++++++++++ 2 files changed, 32 insertions(+) diff --git a/app/preview_test.py b/app/preview_test.py index f67aeb4..d945df1 100644 --- a/app/preview_test.py +++ b/app/preview_test.py @@ -273,6 +273,28 @@ def test_laplace_transforms(self, response, is_latex, latex, sympy): assert result["preview"]["latex"] == latex assert result["preview"]["sympy"] == sympy + @pytest.mark.parametrize( + "response, is_latex, latex, sympy", [ + ("(x+y)*(x+z)", False, "\\left(x + y\\right) \\cdot \\left(x + z\\right)","(x+y)*(x+z)"), + ("(x+y)*x+z", False, "x \\cdot \\left(x + y\\right) + z", "(x+y)*x+z"), + ("x+y*(x+z)", False, "x + y \\cdot \\left(x + z\\right)", "x+y*(x+z)"), + ("[x+y]*[x+z]", False, "\\left(x + y\\right) \\cdot \\left(x + z\\right)", "(x+y)*(x+z)"), + ("[x+y]*x+z", False, "x \\cdot \\left(x + y\\right) + z", "(x+y)*x+z"), + ("x+y*[x+z]", False, "x + y \\cdot \\left(x + z\\right)", "x+y*(x+z)"), + ] + ) + def test_brackets(self, response, is_latex, latex, sympy): + params = { + "is_latex": is_latex, + "strict_syntax": False, + "elementary_functions": True, + "convention": "implicit_higher_precedence", + } + + result = preview_function(response, params) + assert result["preview"]["latex"] == latex + assert result["preview"]["sympy"] == sympy + if __name__ == "__main__": diff --git a/app/utility/expression_utilities.py b/app/utility/expression_utilities.py index 49e9755..95b6f33 100644 --- a/app/utility/expression_utilities.py +++ b/app/utility/expression_utilities.py @@ -156,6 +156,14 @@ def create_expression_set(exprs, params): return list(expr_set) +def convert_bracket_notation(expr): + """ + Accept [] as another way of writing (), SymPy only accepts () for grouping + since [] and {} are reserved for lists and sets. + """ + return expr.replace("[", "(").replace("]", ")") + + def convert_absolute_notation(expr, name): """ Accept || as another form of writing modulus of an expression. @@ -735,6 +743,7 @@ def substitutions_sort_key(x): def preprocess_expression(name, expr, parameters): expr = substitute_input_symbols(expr.strip(), parameters) expr = expr[0] + expr = convert_bracket_notation(expr) expr, abs_feedback = convert_absolute_notation(expr, name) success = True if abs_feedback is not None: @@ -763,6 +772,7 @@ def parse_expression(expr_string, parsing_params): parsed_expr_set = set() for expr in expr_set: + expr = convert_bracket_notation(expr) expr = preprocess_according_to_chosen_convention(expr, parsing_params) substitutions = list(set(substitutions)) From 4211f4bd13dde3a4c48dbc32a7a8b9f827cdf02c Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Wed, 22 Jul 2026 11:39:14 +0100 Subject: [PATCH 2/4] Added strict syntax handling for bracket notation and corresponding tests --- app/preview_test.py | 37 +++++++++++++++++++++++++++++ app/utility/expression_utilities.py | 6 +++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/app/preview_test.py b/app/preview_test.py index 5bb98e6..403854a 100644 --- a/app/preview_test.py +++ b/app/preview_test.py @@ -295,6 +295,43 @@ def test_brackets(self, response, is_latex, latex, sympy): assert result["preview"]["latex"] == latex assert result["preview"]["sympy"] == sympy + @pytest.mark.parametrize( + "response, latex, sympy", [ + ("(x+y)*(x+z)", "\\left(x + y\\right) \\cdot \\left(x + z\\right)", "(x+y)*(x+z)"), + ("(x+y)*x+z", "x \\cdot \\left(x + y\\right) + z", "(x+y)*x+z"), + ("x+y*(x+z)", "x + y \\cdot \\left(x + z\\right)", "x+y*(x+z)"), + ] + ) + def test_brackets_strict_syntax_parentheses_still_work(self, response, latex, sympy): + params = { + "is_latex": False, + "strict_syntax": True, + "elementary_functions": True, + "convention": "implicit_higher_precedence", + } + + result = preview_function(response, params) + assert result["preview"]["latex"] == latex + assert result["preview"]["sympy"] == sympy + + @pytest.mark.parametrize( + "response", [ + "[x+y]*[x+z]", + "[x+y]*x+z", + "x+y*[x+z]", + ] + ) + def test_brackets_rejected_with_strict_syntax(self, response): + params = { + "is_latex": False, + "strict_syntax": True, + "elementary_functions": True, + "convention": "implicit_higher_precedence", + } + + with pytest.raises(ValueError): + preview_function(response, params) + if __name__ == "__main__": pytest.main(['-xk not slow', "--tb=line", os.path.abspath(__file__)]) diff --git a/app/utility/expression_utilities.py b/app/utility/expression_utilities.py index 95b6f33..90e1e16 100644 --- a/app/utility/expression_utilities.py +++ b/app/utility/expression_utilities.py @@ -743,7 +743,8 @@ def substitutions_sort_key(x): def preprocess_expression(name, expr, parameters): expr = substitute_input_symbols(expr.strip(), parameters) expr = expr[0] - expr = convert_bracket_notation(expr) + if not parameters.get("strict_syntax", False): + expr = convert_bracket_notation(expr) expr, abs_feedback = convert_absolute_notation(expr, name) success = True if abs_feedback is not None: @@ -772,7 +773,8 @@ def parse_expression(expr_string, parsing_params): parsed_expr_set = set() for expr in expr_set: - expr = convert_bracket_notation(expr) + if not strict_syntax: + expr = convert_bracket_notation(expr) expr = preprocess_according_to_chosen_convention(expr, parsing_params) substitutions = list(set(substitutions)) From c29858cb6c2c23458e0b5e049a6ff71c6a621d50 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Wed, 22 Jul 2026 15:24:57 +0100 Subject: [PATCH 3/4] Added curly brace notation support and associated strict syntax tests --- app/preview_test.py | 27 +++++++++++++++++++++++++++ app/utility/expression_utilities.py | 7 ++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/app/preview_test.py b/app/preview_test.py index 403854a..e3ad7d4 100644 --- a/app/preview_test.py +++ b/app/preview_test.py @@ -281,6 +281,9 @@ def test_laplace_transforms(self, response, is_latex, latex, sympy): ("[x+y]*[x+z]", False, "\\left(x + y\\right) \\cdot \\left(x + z\\right)", "(x+y)*(x+z)"), ("[x+y]*x+z", False, "x \\cdot \\left(x + y\\right) + z", "(x+y)*x+z"), ("x+y*[x+z]", False, "x + y \\cdot \\left(x + z\\right)", "x+y*(x+z)"), + ("{x+y}*{x+z}", False, "\\left(x + y\\right) \\cdot \\left(x + z\\right)", "(x+y)*(x+z)"), + ("{x+y}*x+z", False, "x \\cdot \\left(x + y\\right) + z", "(x+y)*x+z"), + ("x+y*{x+z}", False, "x + y \\cdot \\left(x + z\\right)", "x+y*(x+z)"), ] ) def test_brackets(self, response, is_latex, latex, sympy): @@ -319,6 +322,12 @@ def test_brackets_strict_syntax_parentheses_still_work(self, response, latex, sy "[x+y]*[x+z]", "[x+y]*x+z", "x+y*[x+z]", + # {x+y}*{x+z} also starts and ends with a curly brace, so with + # conversion skipped it is (mis)detected as a multi-answer wrapper + # by create_expression_set's naive check, garbling the expression. + # This ambiguity with multi-answer {} syntax is a known, deferred + # issue — see is_multiple_answers_wrapper follow-up. + "{x+y}*{x+z}", ] ) def test_brackets_rejected_with_strict_syntax(self, response): @@ -332,6 +341,24 @@ def test_brackets_rejected_with_strict_syntax(self, response): with pytest.raises(ValueError): preview_function(response, params) + @pytest.mark.parametrize( + "response, latex, sympy", [ + ("{x+y}*x+z", "\\left\\{x + y\\right\\} \\cdot x + z", "{x+y}*x+z"), + ("x+y*{x+z}", "x + y \\cdot \\left\\{x + z\\right\\}", "x+y*{x+z}"), + ] + ) + def test_curly_braces_not_converted_with_strict_syntax(self, response, latex, sympy): + params = { + "is_latex": False, + "strict_syntax": True, + "elementary_functions": True, + "convention": "implicit_higher_precedence", + } + + result = preview_function(response, params) + assert result["preview"]["latex"] == latex + assert result["preview"]["sympy"] == sympy + if __name__ == "__main__": pytest.main(['-xk not slow', "--tb=line", os.path.abspath(__file__)]) diff --git a/app/utility/expression_utilities.py b/app/utility/expression_utilities.py index 90e1e16..dcff6c0 100644 --- a/app/utility/expression_utilities.py +++ b/app/utility/expression_utilities.py @@ -158,10 +158,11 @@ def create_expression_set(exprs, params): def convert_bracket_notation(expr): """ - Accept [] as another way of writing (), SymPy only accepts () for grouping - since [] and {} are reserved for lists and sets. + Accept [] and {} as other ways of writing (), SymPy only accepts () + for grouping since [], {}, and () are otherwise reserved for lists, + sets, and tuples respectively. """ - return expr.replace("[", "(").replace("]", ")") + return expr.replace("[", "(").replace("]", ")").replace("{", "(").replace("}", ")") def convert_absolute_notation(expr, name): From 2c573f252a02a6e81ebbdb76562e83957b045e9f Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Wed, 22 Jul 2026 16:17:11 +0100 Subject: [PATCH 4/4] Refined curly brace handling: added `is_multiple_answers_wrapper` utility, updated `create_expression_set`, and included comprehensive test coverage. --- app/preview_test.py | 20 ++++++++++++++------ app/tests/expression_utilities_test.py | 21 +++++++++++++++++++++ app/tests/symbolic_evaluation_test.py | 6 +++--- app/utility/expression_utilities.py | 24 +++++++++++++++++++++--- 4 files changed, 59 insertions(+), 12 deletions(-) diff --git a/app/preview_test.py b/app/preview_test.py index e3ad7d4..eca9121 100644 --- a/app/preview_test.py +++ b/app/preview_test.py @@ -322,12 +322,6 @@ def test_brackets_strict_syntax_parentheses_still_work(self, response, latex, sy "[x+y]*[x+z]", "[x+y]*x+z", "x+y*[x+z]", - # {x+y}*{x+z} also starts and ends with a curly brace, so with - # conversion skipped it is (mis)detected as a multi-answer wrapper - # by create_expression_set's naive check, garbling the expression. - # This ambiguity with multi-answer {} syntax is a known, deferred - # issue — see is_multiple_answers_wrapper follow-up. - "{x+y}*{x+z}", ] ) def test_brackets_rejected_with_strict_syntax(self, response): @@ -343,6 +337,7 @@ def test_brackets_rejected_with_strict_syntax(self, response): @pytest.mark.parametrize( "response, latex, sympy", [ + ("{x+y}*{x+z}", "\\left\\{x + y\\right\\} \\cdot \\left\\{x + z\\right\\}", "{x+y}*{x+z}"), ("{x+y}*x+z", "\\left\\{x + y\\right\\} \\cdot x + z", "{x+y}*x+z"), ("x+y*{x+z}", "x + y \\cdot \\left\\{x + z\\right\\}", "x+y*{x+z}"), ] @@ -359,6 +354,19 @@ def test_curly_braces_not_converted_with_strict_syntax(self, response, latex, sy assert result["preview"]["latex"] == latex assert result["preview"]["sympy"] == sympy + @pytest.mark.parametrize("strict_syntax", [False, True]) + def test_curly_brace_multiple_answers_still_works(self, strict_syntax): + params = { + "is_latex": False, + "strict_syntax": strict_syntax, + "elementary_functions": True, + "convention": "implicit_higher_precedence", + } + + result = preview_function("{1, 2}", params) + assert result["preview"]["latex"] == "\\left\\{1,~2\\right\\}" + assert result["preview"]["sympy"] == "{1, 2}" + if __name__ == "__main__": pytest.main(['-xk not slow', "--tb=line", os.path.abspath(__file__)]) diff --git a/app/tests/expression_utilities_test.py b/app/tests/expression_utilities_test.py index 1d45eb8..4476820 100644 --- a/app/tests/expression_utilities_test.py +++ b/app/tests/expression_utilities_test.py @@ -8,6 +8,7 @@ create_expression_set, extract_latex, find_matching_parenthesis, + is_multiple_answers_wrapper, latex_symbols, preprocess_expression, protect_elementary_functions_substitutions, @@ -356,6 +357,26 @@ def test_plus_minus_expands_to_two_expressions(self): assert sorted(result) == sorted(["+x", "-x"]) or sorted(result) == sorted(["x", "-x"]) assert len(result) == 2 + def test_curly_braces_used_for_grouping_are_not_split(self): + result = create_expression_set("{x+1}*{x-2}", {}) + assert result == ["{x+1}*{x-2}"] + + +class TestIsMultipleAnswersWrapper: + + @pytest.mark.parametrize( + "expr, expected", [ + ("{x, y}", True), + ("{x+1}", True), + ("{(x+1), (x-1)}", True), + ("x+y", False), + ("{x+1}*{x-2}", False), + ("{x+1}*x", False), + ] + ) + def test_is_multiple_answers_wrapper(self, expr, expected): + assert is_multiple_answers_wrapper(expr) is expected + class TestPreprocessExpression: diff --git a/app/tests/symbolic_evaluation_test.py b/app/tests/symbolic_evaluation_test.py index 0dfec9c..248db37 100644 --- a/app/tests/symbolic_evaluation_test.py +++ b/app/tests/symbolic_evaluation_test.py @@ -1956,8 +1956,8 @@ def test_elementary_function_symbol_with_implicit_multiplication_on_both_sides(s result = evaluation_function(response, answer, params) assert result["is_correct"] is True - def test_response_for_which_correctness_cannot_be_determined(self): - response = "2 pi e^{-a |omega|}" # The expression in {...} is interpreted as elements in a set instead of a math expression + def test_curly_braces_used_for_grouping_in_response(self): + response = "2 pi e^{-a |omega|}" # {} is accepted as another way of writing () for grouping answer = "2 pi e^(-a|omega|)" params = { 'atol': 0, @@ -1967,7 +1967,7 @@ def test_response_for_which_correctness_cannot_be_determined(self): 'elementary_functions': True, } result = evaluation_function(response, answer, params) - assert result["is_correct"] is False + assert result["is_correct"] is True def test_unexpected_equalities_in_response_that_generates_set(self): response = "z= plus_minus 1 + 2*i" # plus_minus generates a set of two equalities diff --git a/app/utility/expression_utilities.py b/app/utility/expression_utilities.py index dcff6c0..b915bc5 100644 --- a/app/utility/expression_utilities.py +++ b/app/utility/expression_utilities.py @@ -129,9 +129,22 @@ def _print_log(self, expr, exp=None): # -------- String Manipulation Utilities +def is_multiple_answers_wrapper(expr): + """ + True if expr, as a whole, is wrapped in a top-level {...} pair that + create_expression_set will treat as a set of multiple acceptable + answers, as opposed to {} that merely sit at the string's edges + incidentally (e.g. "{x+1}*{x-2}"). + """ + stripped = expr.strip() + if not (stripped.startswith('{') and stripped.endswith('}')): + return False + return find_matching_parenthesis(stripped, 0, delimiters=('{', '}')) == len(stripped) - 1 + + def create_expression_set(exprs, params): if isinstance(exprs, str): - if exprs.startswith('{') and exprs.endswith('}'): + if is_multiple_answers_wrapper(exprs): exprs = [expr.strip() for expr in exprs[1:-1].split(',')] else: exprs = [exprs] @@ -160,9 +173,14 @@ def convert_bracket_notation(expr): """ Accept [] and {} as other ways of writing (), SymPy only accepts () for grouping since [], {}, and () are otherwise reserved for lists, - sets, and tuples respectively. + sets, and tuples respectively. {} is left untouched when it spans the + entire expression, since that denotes a set of multiple acceptable + answers (see create_expression_set). """ - return expr.replace("[", "(").replace("]", ")").replace("{", "(").replace("}", ")") + expr = expr.replace("[", "(").replace("]", ")") + if is_multiple_answers_wrapper(expr): + return expr + return expr.replace("{", "(").replace("}", ")") def convert_absolute_notation(expr, name):