diff --git a/expression_parsing/CHANGELOG.md b/expression_parsing/CHANGELOG.md index 7866566..7e963f6 100644 --- a/expression_parsing/CHANGELOG.md +++ b/expression_parsing/CHANGELOG.md @@ -42,6 +42,7 @@ The first public release. Further tidy-up after the 0.2.0 extraction: previewing ### Added - `SympyParsingConfig.replace` also takes `extra_unsplittable_symbols` and `extra_symbol_assumptions`, alongside parameter changes (`config.replace(simplify=False, extra_unsplittable_symbols=(...))`); they needed `dataclasses.replace` before. +- `notation_warnings(response, strict_syntax)` returns the `NotationWarning`s for a response: `EXPONENT` (`^`) and `FACTORIAL` (`!`) under strict syntax, and `TRIPLE_FACTORIAL` (`!!!`) always. `NotationWarning` is a `StrEnum` whose values are compareExpressions' feedback tags (`NOTATION_WARNING_EXPONENT`, ...), so a tag lookup keeps working. Wording the warnings is still up to the caller. It was `notation_warnings` in compareExpressions' `evaluation.py`, and behaves the same: the checks are substring tests, so under strict syntax `!=` also warns `FACTORIAL`, and `!!!` warns both `FACTORIAL` and `TRIPLE_FACTORIAL`. - DEBUG logging through module loggers (`lambdafeedback.expression_parsing.*`), with a `NullHandler` on the package logger; nothing is logged at INFO or above. See the repository README. Preprocessing, parsing and `parse_latex` log each step's result. ### Security diff --git a/expression_parsing/src/lambdafeedback/expression_parsing/__init__.py b/expression_parsing/src/lambdafeedback/expression_parsing/__init__.py index 7f3eb79..32b06ed 100644 --- a/expression_parsing/src/lambdafeedback/expression_parsing/__init__.py +++ b/expression_parsing/src/lambdafeedback/expression_parsing/__init__.py @@ -31,6 +31,7 @@ UnknownConventionError, ) from .latex import MAX_LATEX_NESTING, extract_latex, latex_symbols, parse_latex, sanitise_latex, sympy_to_latex +from .notation import NotationWarning, notation_warnings from .numbers import ( compute_relative_tolerance_from_significant_decimals, generate_arbitrary_number_pattern_matcher, @@ -82,6 +83,7 @@ "FunctionName", "InvalidParameterError", "LatexParseError", + "NotationWarning", "SymbolAssumptionError", "SymbolSpec", "SympyParsingConfig", @@ -103,6 +105,7 @@ "is_number", "latex_symbols", "normalise_unicode_operators", + "notation_warnings", "parse_expression", "parse_expressions", "parse_latex", diff --git a/expression_parsing/src/lambdafeedback/expression_parsing/notation.py b/expression_parsing/src/lambdafeedback/expression_parsing/notation.py new file mode 100644 index 0000000..492d6b4 --- /dev/null +++ b/expression_parsing/src/lambdafeedback/expression_parsing/notation.py @@ -0,0 +1,37 @@ +"""Notation in a response that is likely to be misread, such as ``^`` for a power under strict syntax. + +Only the detection lives here: wording the warnings for a learner is up to +the caller, keyed by :class:`NotationWarning`. +""" + +from __future__ import annotations + +from enum import StrEnum + + +class NotationWarning(StrEnum): + """A notation warning; the values are compareExpressions' feedback tags.""" + + EXPONENT = "NOTATION_WARNING_EXPONENT" + """``^`` under strict syntax, which doesn't read it as a power.""" + FACTORIAL = "NOTATION_WARNING_FACTORIAL" + """``!`` under strict syntax, which doesn't read it as a factorial.""" + TRIPLE_FACTORIAL = "NOTATION_WARNING_TRIPLE_FACTORIAL" + """``!!!``, which is never supported.""" + + +def notation_warnings(response: str, strict_syntax: bool) -> list[NotationWarning]: + """The notation warnings for ``response``, in the order of :class:`NotationWarning`. + + The checks look for the characters anywhere in ``response``, so under + strict syntax ``!`` also warns for ``!=`` and ``!!!`` (which gets both + factorial warnings). + """ + warnings = [] + if strict_syntax and "^" in response: + warnings.append(NotationWarning.EXPONENT) + if strict_syntax and "!" in response: + warnings.append(NotationWarning.FACTORIAL) + if "!!!" in response: + warnings.append(NotationWarning.TRIPLE_FACTORIAL) + return warnings diff --git a/expression_parsing/tests/test_api.py b/expression_parsing/tests/test_api.py index ee0d18a..7723da2 100644 --- a/expression_parsing/tests/test_api.py +++ b/expression_parsing/tests/test_api.py @@ -8,6 +8,7 @@ ExpressionParsingError, InvalidParameterError, LatexParseError, + NotationWarning, SymbolAssumptionError, SymbolSpec, SympyParsingConfig, @@ -17,6 +18,7 @@ is_complex_number_on_cartesian_form, is_complex_number_on_exponential_form, is_number, + notation_warnings, parse_expression, parse_expressions, parse_latex, @@ -185,3 +187,29 @@ def test_number_forms(): assert is_complex_number_on_cartesian_form("1+2*I") assert is_complex_number_on_exponential_form("2*exp(3*I)") assert not is_number("x") + + +class TestNotationWarnings: + @pytest.mark.parametrize( + "response, strict, expected", + [ + ("x**2", True, []), + ("x^2", True, [NotationWarning.EXPONENT]), + ("x^2", False, []), + ("n!", True, [NotationWarning.FACTORIAL]), + ("n!", False, []), + ("x != y", True, [NotationWarning.FACTORIAL]), + ("n!!!", False, [NotationWarning.TRIPLE_FACTORIAL]), + ("n!!!", True, [NotationWarning.FACTORIAL, NotationWarning.TRIPLE_FACTORIAL]), + ("x^n!", True, [NotationWarning.EXPONENT, NotationWarning.FACTORIAL]), + ], + ) + def test_warnings(self, response, strict, expected): + assert notation_warnings(response, strict) == expected + + def test_values_are_the_compare_expressions_tags(self): + assert [str(warning) for warning in NotationWarning] == [ + "NOTATION_WARNING_EXPONENT", + "NOTATION_WARNING_FACTORIAL", + "NOTATION_WARNING_TRIPLE_FACTORIAL", + ]