Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/feedback/symbolic.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
feedback_generators["EQUIVALENCES"] = criteria_equivalences
feedback_generators["INTERNAL"] = lambda tag: lambda inputs: {
"ABSOLUTE_VALUE_NOTATION_AMBIGUITY": f"Notation in {inputs.get('name','')} might be ambiguous, use `Abs(.)` instead of `|.|`",
"BRACKET_NOTATION_MISMATCH": f"Brackets in `{inputs.get('x','')}` do not match. Every `(`, `[` or `{{` must be closed with the corresponding `)`, `]` or `}}` — different bracket types cannot be mixed to close the same group.",
"NO_RESPONSE": "No response submitted.",
"MULTIPLE_ANSWER_FAIL_ALL": "At least one answer or response was incorrect.",
"MULTIPLE_ANSWER_FAIL_RESPONSE": "At least one response was incorrect.",
Expand Down
86 changes: 85 additions & 1 deletion app/tests/expression_utilities_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
from ..utility.expression_utilities import (
compute_relative_tolerance_from_significant_decimals,
convert_absolute_notation,
convert_bracket_notation,
convert_unicode_dashes,
create_expression_set,
extract_latex,
find_matching_parenthesis,
has_matching_brackets,
is_multiple_answers_wrapper,
latex_symbols,
preprocess_expression,
Expand Down Expand Up @@ -183,6 +185,66 @@ def test_find_matching_parenthesis(self, string, index, delimiters, expected):
assert result == expected


class TestHasMatchingBrackets:

@pytest.mark.parametrize(
"expr, expected", [
("x+y", True),
("(x+y)", True),
("[x+y]", True),
("{x+y}", True),
("[x+(y-1)]", True),
("{(x+1), (x-1)}", True),
# Mismatched bracket types
("[x+y)", False),
("(x+y]", False),
("{x+y)", False),
("[(x+y]", False),
# Unbalanced brackets
("[x+y", False),
("x+y]", False),
("((x+y)", False),
]
)
def test_has_matching_brackets(self, expr, expected):
assert has_matching_brackets(expr) is expected


class TestConvertBracketNotation:

@pytest.mark.parametrize(
"expr, expected", [
("[x+y]", "(x+y)"),
("[x+(y-1)]", "(x+(y-1))"),
("{x+1}*{x-2}", "(x+1)*(x-2)"),
]
)
def test_matched_brackets_are_converted(self, expr, expected):
result, feedback = convert_bracket_notation(expr)
assert result == expected
assert feedback is None

def test_multiple_answers_wrapper_left_untouched(self):
result, feedback = convert_bracket_notation("{x+1, x-1}")
assert result == "{x+1, x-1}"
assert feedback is None

@pytest.mark.parametrize(
"expr", [
"[x+y)",
"(x+y]",
"{x+y)",
"[x+y",
"x+y]",
]
)
def test_mismatched_brackets_are_rejected(self, expr):
result, feedback = convert_bracket_notation(expr)
assert result == expr
assert feedback is not None
assert feedback[0] == "BRACKET_NOTATION_MISMATCH"


class TestSubstitute:

@pytest.mark.parametrize(
Expand Down Expand Up @@ -396,4 +458,26 @@ def test_ambiguous_pipes_returns_failure(self):
success, expr, feedback = preprocess_expression("response", "|x|y|z|", {})
assert success is False
assert feedback is not None
assert feedback[0] == "ABSOLUTE_VALUE_NOTATION_AMBIGUITY"
assert feedback[0] == "ABSOLUTE_VALUE_NOTATION_AMBIGUITY"

def test_square_brackets_converted(self):
success, expr, feedback = preprocess_expression("response", "[x+y]", {})
assert success is True
assert expr == "(x+y)"
assert feedback is None

@pytest.mark.parametrize(
"expr", [
"[x+y)",
"(x+y]",
"{x+y)",
"[x+y",
"x+y]",
]
)
def test_mismatched_brackets_returns_failure(self, expr):
success, result, feedback = preprocess_expression("response", expr, {})
assert success is False
assert result == expr
assert feedback is not None
assert feedback[0] == "BRACKET_NOTATION_MISMATCH"
42 changes: 34 additions & 8 deletions app/utility/expression_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,20 @@ def convert_bracket_notation(expr):
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).

Brackets must be closed with the matching type, e.g. "[x+y)" is
rejected rather than silently reinterpreted as "(x+y)", since this
project does not support interval/limit notation. If the brackets in
expr are mismatched, expr is returned unconverted together with
feedback describing the problem.
"""
if not has_matching_brackets(expr):
tag = "BRACKET_NOTATION_MISMATCH"
return expr, (tag, feedback_string_generators["INTERNAL"](tag)({'x': expr}))
expr = expr.replace("[", "(").replace("]", ")")
if is_multiple_answers_wrapper(expr):
return expr
return expr.replace("{", "(").replace("}", ")")
return expr, None
return expr.replace("{", "(").replace("}", ")"), None


def convert_absolute_notation(expr, name):
Expand Down Expand Up @@ -467,6 +476,23 @@ def substitute_input_symbols(exprs, params):
return exprs


def has_matching_brackets(expr):
"""
True if every occurrence of (, [, { in expr is closed by the closing
bracket of the same type, correctly nested. Mismatched types (e.g. the
"[x+y)" in interval/limit notation) or unbalanced brackets are rejected.
"""
closing_to_opening = {')': '(', ']': '[', '}': '{'}
stack = []
for char in expr:
if char in '([{':
stack.append(char)
elif char in ')]}':
if not stack or stack.pop() != closing_to_opening[char]:
return False
return not stack


def find_matching_parenthesis(string, index, delimiters=None):
depth = 0
if delimiters is None:
Expand Down Expand Up @@ -762,13 +788,13 @@ def substitutions_sort_key(x):
def preprocess_expression(name, expr, parameters):
expr = substitute_input_symbols(expr.strip(), parameters)
expr = expr[0]
bracket_feedback = None
if not parameters.get("strict_syntax", False):
expr = convert_bracket_notation(expr)
expr, bracket_feedback = convert_bracket_notation(expr)
expr, abs_feedback = convert_absolute_notation(expr, name)
success = True
if abs_feedback is not None:
success = False
return success, expr, abs_feedback
feedback = bracket_feedback or abs_feedback
success = feedback is None
return success, expr, feedback

def parse_expression(expr_string, parsing_params):
'''
Expand All @@ -793,7 +819,7 @@ def parse_expression(expr_string, parsing_params):

for expr in expr_set:
if not strict_syntax:
expr = convert_bracket_notation(expr)
expr, _ = convert_bracket_notation(expr)
expr = preprocess_according_to_chosen_convention(expr, parsing_params)

substitutions = list(set(substitutions))
Expand Down
Loading