diff --git a/mathics/builtin/forms/data.py b/mathics/builtin/forms/data.py index 374209721..3cc8f061e 100644 --- a/mathics/builtin/forms/data.py +++ b/mathics/builtin/forms/data.py @@ -10,35 +10,37 @@ which are intended to work over all kinds of data. """ import re +from typing import Any, Callable, Dict, List, Optional from mathics.builtin.box.layout import RowBox, to_boxes from mathics.builtin.forms.base import FormBaseClass from mathics.builtin.makeboxes import MakeBoxes from mathics.core.atoms import Integer, Real, String from mathics.core.builtin import Builtin -from mathics.core.element import EvalMixin +from mathics.core.element import BaseElement, EvalMixin from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.number import dps -from mathics.core.symbols import Symbol, SymbolFalse, SymbolNull, SymbolTrue +from mathics.core.symbols import Atom, Symbol, SymbolFalse, SymbolNull, SymbolTrue from mathics.core.systemsymbols import ( SymbolAutomatic, SymbolInfinity, SymbolMakeBoxes, - SymbolNumberForm, SymbolRowBox, - SymbolRuleDelayed, SymbolSuperscriptBox, ) from mathics.eval.makeboxes import ( - NumberForm_to_String, StringLParen, StringRParen, eval_baseform, + eval_generic_makeboxes, eval_tableform, + format_element, + get_numberform_parameters, + numberform_to_boxes, ) -from mathics.eval.strings import eval_ToString +from mathics.eval.strings import eval_StringForm_MakeBoxes, eval_ToString class BaseForm(FormBaseClass): @@ -96,7 +98,7 @@ class BaseForm(FormBaseClass): def eval_makeboxes(self, expr, n, f, evaluation: Evaluation): """MakeBoxes[BaseForm[expr_, n_], f:StandardForm|TraditionalForm|OutputForm]""" - return eval_baseform(self, expr, n, f, evaluation) + return eval_baseform(expr, n, f, evaluation) class _NumberForm(Builtin): @@ -108,32 +110,78 @@ class _NumberForm(Builtin): default_NumberFormat = None in_outputforms = True messages = { - "npad": "Value for option NumberPadding -> `1` should be a string or a pair of strings.", - "dblk": "Value for option DigitBlock should be a positive integer, Infinity, or a pair of positive integers.", + "argm": ("`` called with `` arguments; 1 or more " "arguments are expected."), + "argct": "`` called with `` arguments.", + "npad": ( + "Value for option NumberPadding -> `1` should be a string or " + "a pair of strings." + ), + "dblk": ( + "Value for option DigitBlock should be a positive integer, " + "Infinity, or a pair of positive integers." + ), + "estep": "Value of option `1` -> `2` is not a positive integer.", + "iprf": ( + "Formatting specification `1` should be a positive integer " + "or a pair of positive integers." + ), # NumberFormat only "npt": "Value for option `1` -> `2` is expected to be a string.", - "nsgn": "Value for option NumberSigns -> `1` should be a pair of strings or two pairs of strings.", - "nspr": "Value for option NumberSeparator -> `1` should be a string or a pair of strings.", + "nsgn": ( + "Value for option NumberSigns -> `1` should be a pair of " + "strings or two pairs of strings." + ), + "nspr": ( + "Value for option NumberSeparator -> `1` should be a string " + "or a pair of strings." + ), "opttf": "Value of option `1` -> `2` should be True or False.", - "estep": "Value of option `1` -> `2` is not a positive integer.", - "iprf": "Formatting specification `1` should be a positive integer or a pair of positive integers.", # NumberFormat only - "sigz": "In addition to the number of digits requested, one or more zeros will appear as placeholders.", + "sigz": ( + "In addition to the number of digits requested, one or more " + "zeros will appear as placeholders." + ), } - def check_options(self, options: dict, evaluation: Evaluation): + def check_and_convert_options(self, options: dict, evaluation: Evaluation): """ Checks options are valid and converts them to python. """ result = {} + default_options = evaluation.definitions.get_options(self.get_name()) for option_name in self.options: + context_option_name = "System`" + option_name method = getattr(self, "check_" + option_name) - arg = options["System`" + option_name] + arg = options[context_option_name] value = method(arg, evaluation) - if value is None: - return None + if value is not None: + result[option_name] = value + continue + # If the value is None, try with the default value + arg = default_options[context_option_name] + value = method(arg, evaluation) + # If fails, handle None in situ. result[option_name] = value + return result - def check_DigitBlock(self, value, evaluation: Evaluation): + def check_DigitBlock(self, value, evaluation: Evaluation) -> Optional[List[int]]: + """ + Check and convert to Python the DigitBlock option value. + + Parameters + ---------- + value : BaseElement + The value of the option. + evaluation : Evaluation + used for messages. + + Returns + ------- + Optional[List[int]] + If the specification is valid, a list with + two elements specifying the size of the blocks + at the left and right of the decimal separator. `None` otherwise. + + """ py_value = value.get_int_value() if value.sameQ(SymbolInfinity): return [0, 0] @@ -158,8 +206,29 @@ def check_DigitBlock(self, value, evaluation: Evaluation): if None not in result: return result evaluation.message(self.get_name(), "dblk", value) + return None + + def check_ExponentFunction( + self, value: BaseElement, evaluation: Evaluation + ) -> Callable[BaseElement, BaseElement]: + """ + Check and convert the ExponentFunction option value + + Parameters + ---------- + value : BaseElement + Automatic, or a Function to be applyied to the expression to + format the exponent. + evaluation : Evaluation + evaluation object to send messages. + + Returns + ------- + Callable[BaseElement, BaseElement] + A Python function that implements the format. + + """ - def check_ExponentFunction(self, value, evaluation: Evaluation): if value.sameQ(SymbolAutomatic): return self.default_ExponentFunction @@ -168,60 +237,251 @@ def exp_function(x): return exp_function - def check_NumberFormat(self, value, evaluation: Evaluation): + def check_NumberFormat( + self, value: BaseElement, evaluation: Evaluation + ) -> Callable[BaseElement, BaseElement]: + """ + Function that implement custumozed number formatting. + + Parameters + ---------- + value : BaseElement + Automatic, or a function to be applied to the expression to get + it formatted. + evaluation : Evaluation + Evaluation object used to show messages. + + Returns + ------- + Callable[BaseElement, BaseElement] + A function that implements the formatting. + + """ if value.sameQ(SymbolAutomatic): return self.default_NumberFormat - def num_function(man, base, exp, options): + def num_function(man, base, exp, _): return Expression(value, man, base, exp).evaluate(evaluation) return num_function - def check_NumberMultiplier(self, value, evaluation: Evaluation): + def check_NumberMultiplier( + self, value: BaseElement, evaluation: Evaluation + ) -> Optional[str]: + """ + Character used when two numbers are multiplied. Used in Scientific + notation. + + Parameters + ---------- + value : BaseElement + Value of the option. + evaluation : Evaluation + Evaluation object used to show messages. + + Returns + ------- + Optional[str] + If the value is valid, the value of the option. `None` otherwise. + + """ result = value.get_string_value() if result is None: evaluation.message(self.get_name(), "npt", "NumberMultiplier", value) return result - def check_NumberPoint(self, value, evaluation: Evaluation): + def check_NumberPoint( + self, value: BaseElement, evaluation: Evaluation + ) -> Optional[str]: + """ + The decimal separator + + Parameters + ---------- + value : BaseElement + Option value. + evaluation : Evaluation + Evaluation object used to show messages. + + Returns + ------- + Optional[str] + If the value is valid, the value of the option. `None` otherwise. + + """ result = value.get_string_value() if result is None: evaluation.message(self.get_name(), "npt", "NumberPoint", value) return result - def check_ExponentStep(self, value, evaluation: Evaluation): + def check_ExponentStep( + self, value: BaseElement, evaluation: Evaluation + ) -> Optional[int]: + """ + The round step for exponents in Scientific notation. This number + decides for example if format 10000 as "10x10^3" or "1x10^4" + + Parameters + ---------- + value : BaseElemenet + The value of the option. + evaluation : Evaluation + Evaluation object used to show messages. + + Returns + ------- + Optional[int] + If the value is valid, the value of the option. `None` otherwise. + + """ result = value.get_int_value() if result is None or result <= 0: evaluation.message(self.get_name(), "estep", "ExponentStep", value) - return + return None return result - def check_SignPadding(self, value, evaluation: Evaluation): + def check_SignPadding( + self, value: BaseElement, evaluation: Evaluation + ) -> Optional[bool]: + """ + True if the left padding is used between the sign of the number of + its magnitude. False otherwise. + + Parameters + ---------- + value : BaseElement + The value of the option. + evaluation : Evaluation + Evaluation object used to show messages. + + Returns + ------- + Optional[bool] + If the value is valid, the value of the option. `None` otherwise. + + """ if value.sameQ(SymbolTrue): return True - elif value.sameQ(SymbolFalse): + if value.sameQ(SymbolFalse): return False evaluation.message(self.get_name(), "opttf", value) + return None - def _check_List2str(self, value, msg, evaluation: Evaluation): + def _check_List2str( + self, value, msg, evaluation: Evaluation + ) -> Optional[List[str]]: if value.has_form("List", 2): result = [element.get_string_value() for element in value.elements] if None not in result: return result evaluation.message(self.get_name(), msg, value) + return None + + def check_NumberSigns( + self, value: BaseElement, evaluation: Evaluation + ) -> Optional[List[str]]: + """ + + Parameters + ---------- + value : BaseElement + The value of the option. + evaluation : Evaluation + Evaluation object used to show messages. + + Returns + ------- + Optional[bool] + If the value is valid, the value of the option. `None` otherwise. + + """ - def check_NumberSigns(self, value, evaluation: Evaluation): return self._check_List2str(value, "nsgn", evaluation) - def check_NumberPadding(self, value, evaluation: Evaluation): + def check_NumberPadding( + self, value: BaseElement, evaluation: Evaluation + ) -> Optional[List[str]]: + """ + + Parameters + ---------- + value : BaseElement + The value of the option. + evaluation : Evaluation + Evaluation object used to show messages. + + Returns + ------- + Optional[bool] + If the value is valid, the value of the option. `None` otherwise. + + """ + return self._check_List2str(value, "npad", evaluation) - def check_NumberSeparator(self, value, evaluation: Evaluation): + def check_NumberSeparator( + self, value: BaseElement, evaluation: Evaluation + ) -> Optional[List[str]]: + """ + + Parameters + ---------- + value : BaseElement + The value of the option. + evaluation : Evaluation + Evaluation object used to show messages. + + Returns + ------- + Optional[bool] + If the value is valid, the value of the option. `None` otherwise. + + """ + py_str = value.get_string_value() if py_str is not None: return [py_str, py_str] return self._check_List2str(value, "nspr", evaluation) + def eval_number_makeboxes_nonatomic(self, expr, form, evaluation): + """MakeBoxes[expr_%(name)s, form_]""" + # Generic form. If parameters are OK, + # distribute the form on the head and elements. + # If the expression is an Atom, leave it alone. + # First, collect the parts of the expression + num_form = expr.head + try: + target, prec_parms, _ = get_numberform_parameters(expr, evaluation) + except ValueError: + return eval_generic_makeboxes(expr, form, evaluation) + + # Atoms are not processed here + if isinstance(target, Atom): + return None + + # This part broadcast the format to the head and elements + # of the expression. In the future, this is going to happend + # by passing parameters to a makeboxes evaluation function. + + if prec_parms is None: + option_rules = expr.elements[1:] + + def wrapper(elem): + return Expression(num_form, elem, *option_rules) + + else: + option_rules = expr.elements[2:] + + def wrapper(elem): + return Expression(num_form, elem, prec_parms, *option_rules) + + head = target.head + if not isinstance(head, Symbol): + head = wrapper(target.head) + elements = (wrapper(elem) for elem in target.elements) + expr = Expression(head, *elements) + return format_element(expr, evaluation, form) + class NumberForm(_NumberForm): """ @@ -260,18 +520,46 @@ class NumberForm(_NumberForm): "NumberSigns": '{"-", ""}', "SignPadding": "False", } - summary_text = "format expression to at most a number of digits of all approximate real numbers " + summary_text = ( + "format expression to at most a number of digits of all " + "approximate real numbers " + ) @staticmethod - def default_ExponentFunction(value): + def default_ExponentFunction(value: Integer): + """The default function used to format exponent.""" + n = value.get_int_value() if -5 <= n <= 5: return SymbolNull - else: - return value + + return value @staticmethod - def default_NumberFormat(man, base, exp, options): + def default_NumberFormat( + man: BaseElement, base: BaseElement, exp: BaseElement, options: Dict[str, Any] + ) -> BaseElement: + """ + The default function used to format numbers from its mantisa, and + an exponential factor base^exp. + + Parameters + ---------- + man : BaseElement + mantisa. + base : BaseElement + base used for scientific notation. + exp : BaseElement + exponent. + options : Dict[str, Any] + more format options. + + Returns + ------- + Expression + An valid box expression representing the number. + """ + py_exp = exp.get_string_value() if py_exp: mul = String(options["NumberMultiplier"]) @@ -279,101 +567,44 @@ def default_NumberFormat(man, base, exp, options): SymbolRowBox, ListExpression(man, mul, Expression(SymbolSuperscriptBox, base, exp)), ) - else: - return man - - def eval_list_n(self, expr, n, evaluation, options) -> Expression: - "NumberForm[expr_List, n_, OptionsPattern[NumberForm]]" - options = [ - Expression(SymbolRuleDelayed, Symbol(key), value) - for key, value in options.items() - ] - return ListExpression( - *[ - Expression(SymbolNumberForm, element, n, *options) - for element in expr.elements - ] - ) - def eval_list_nf(self, expr, n, f, evaluation, options) -> Expression: - "NumberForm[expr_List, {n_, f_}, OptionsPattern[NumberForm]]" - options = [ - Expression(SymbolRuleDelayed, Symbol(key), value) - for key, value in options.items() - ] - return ListExpression( - *[ - Expression(SymbolNumberForm, element, ListExpression(n, f), *options) - for element in expr.elements - ], - ) + return man - def eval_makeboxes(self, expr, form, evaluation, options={}): - """MakeBoxes[NumberForm[expr_, OptionsPattern[NumberForm]], + def eval_makeboxes(self, fexpr, form, evaluation): + """MakeBoxes[fexpr:NumberForm[_?AtomQ, ___], form:StandardForm|TraditionalForm|OutputForm]""" - - fallback = Expression(SymbolMakeBoxes, expr, form) - - py_options = self.check_options(options, evaluation) - if py_options is None: - return fallback - - if isinstance(expr, Integer): - py_n = len(str(abs(expr.get_int_value()))) - elif isinstance(expr, Real): - if expr.is_machine_precision(): - py_n = 6 - else: - py_n = dps(expr.get_precision()) - else: - py_n = None + try: + target, prec_parms, py_options = get_numberform_parameters( + fexpr, evaluation + ) + except ValueError: + return eval_generic_makeboxes(fexpr, form, evaluation) + + assert all(isinstance(key, str) for key in py_options) + + py_f = py_n = None + if prec_parms is None: + if isinstance(target, Integer): + py_n = len(str(abs(target.get_int_value()))) + elif isinstance(target, Real): + if target.is_machine_precision(): + py_n = 6 + else: + py_n = dps(target.get_precision()) + elif isinstance(prec_parms, Integer): + if isinstance(target, (Integer, Real)): + py_n = prec_parms.value + elif prec_parms.has_form("List", 2): + if isinstance(target, (Integer, Real)): + n, f = prec_parms.elements + py_n = n.value + py_f = f.value if py_n is not None: py_options["_Form"] = form.get_name() - return NumberForm_to_String(expr, py_n, None, evaluation, py_options) - return Expression(SymbolMakeBoxes, expr, form) - - def eval_makeboxes_n(self, expr, n, form, evaluation, options={}): - """MakeBoxes[NumberForm[expr_, n_?NotOptionQ, OptionsPattern[NumberForm]], - form:StandardForm|TraditionalForm|OutputForm]""" - - fallback = Expression(SymbolMakeBoxes, expr, form) - - py_n = n.get_int_value() - if py_n is None or py_n <= 0: - evaluation.message("NumberForm", "iprf", n) - return fallback - - py_options = self.check_options(options, evaluation) - if py_options is None: - return fallback - - if isinstance(expr, (Integer, Real)): - py_options["_Form"] = form.get_name() - return NumberForm_to_String(expr, py_n, None, evaluation, py_options) - return Expression(SymbolMakeBoxes, expr, form) - - def eval_makeboxes_nf(self, expr, n, f, form, evaluation, options={}): - """MakeBoxes[NumberForm[expr_, {n_, f_}, OptionsPattern[NumberForm]], - form:StandardForm|TraditionalForm|OutputForm]""" - fallback = Expression(SymbolMakeBoxes, expr, form) - - nf = ListExpression(n, f) - py_n = n.get_int_value() - py_f = f.get_int_value() - if py_n is None or py_n <= 0 or py_f is None or py_f < 0: - evaluation.message("NumberForm", "iprf", nf) - return fallback - - py_options = self.check_options(options, evaluation) - if py_options is None: - return fallback - - if isinstance(expr, (Integer, Real)): - py_options["_Form"] = form.get_name() - return NumberForm_to_String(expr, py_n, py_f, evaluation, py_options) - return Expression(SymbolMakeBoxes, expr, form) + return numberform_to_boxes(target, py_n, py_f, evaluation, py_options) + return Expression(SymbolMakeBoxes, target, form) class SequenceForm(FormBaseClass): @@ -437,40 +668,16 @@ class StringForm(FormBaseClass): in_outputforms = False in_printforms = False + messages = { + "sfr": 'Item `1` requested in "`3`" out of range; `2` items available.', + "sfq": "Unmatched backquote in `1`.", + } summary_text = "format a string from a template and a list of parameters" def eval_makeboxes(self, s, args, form, evaluation): """MakeBoxes[StringForm[s_String, args___], form:StandardForm|TraditionalForm|OutputForm]""" - - s = s.value - args = args.get_sequence() - result = [] - pos = 0 - last_index = 0 - for match in re.finditer(r"(\`(\d*)\`)", s): - start, end = match.span(1) - if match.group(2): - index = int(match.group(2)) - else: - index = last_index + 1 - last_index = max(index, last_index) - if start > pos: - result.append(to_boxes(String(s[pos:start]), evaluation)) - pos = end - if 1 <= index <= len(args): - arg = args[index - 1] - result.append( - to_boxes(MakeBoxes(arg, form).evaluate(evaluation), evaluation) - ) - if pos < len(s): - result.append(to_boxes(String(s[pos:]), evaluation)) - return RowBox( - *tuple( - r.evaluate(evaluation) if isinstance(r, EvalMixin) else r - for r in result - ) - ) + return eval_StringForm_MakeBoxes(s, args.get_sequence(), form, evaluation) class TableForm(FormBaseClass): @@ -553,8 +760,7 @@ def eval_makeboxes_matrix(self, table, form, evaluation, options): """MakeBoxes[%(name)s[table_, OptionsPattern[%(name)s]], form:StandardForm|TraditionalForm]""" - result = super(MatrixForm, self).eval_makeboxes( - table, form, evaluation, options - ) + result = super().eval_makeboxes(table, form, evaluation, options) if result.get_head_name() == "System`GridBox": return RowBox(StringLParen, result, StringRParen) + return None diff --git a/mathics/builtin/forms/print.py b/mathics/builtin/forms/print.py index ce72b954c..2187fd5b8 100644 --- a/mathics/builtin/forms/print.py +++ b/mathics/builtin/forms/print.py @@ -198,8 +198,16 @@ class OutputForm(FormBaseClass): = -Graphics- """ + formats = {"OutputForm[s_String]": "s"} summary_text = "format expression in plain text" + def eval_makeboxes(self, expr, form, evaluation): + """MakeBoxes[OutputForm[expr_], form_]""" + pane = eval_makeboxes_outputform(expr, evaluation, form) + return InterpretationBox( + pane, Expression(SymbolOutputForm, expr), **{"System`Editable": SymbolFalse} + ) + class StandardForm(FormBaseClass): """ diff --git a/mathics/builtin/list/predicates.py b/mathics/builtin/list/predicates.py index f7b3a6b84..f80cf84c0 100644 --- a/mathics/builtin/list/predicates.py +++ b/mathics/builtin/list/predicates.py @@ -51,18 +51,8 @@ class ContainsOnly(Builtin): summary_text = "test if all the elements of a list appears into another list" - def check_options(self, expr, evaluation, options): - for key in options: - if key != "System`SameTest": - if expr is None: - evaluation.message("ContainsOnly", "optx", Symbol(key)) - else: - evaluation.message("ContainsOnly", "optx", Symbol(key), expr) - - return None - def eval(self, list1, list2, evaluation, options={}): - "ContainsOnly[list1_List, list2_List, OptionsPattern[ContainsOnly]]" + "ContainsOnly[list1_List, list2_List, OptionsPattern[]]" same_test = self.get_option(options, "SameTest", evaluation) @@ -71,31 +61,28 @@ def sameQ(a, b) -> bool: result = Expression(same_test, a, b).evaluate(evaluation) return result is SymbolTrue - self.check_options(None, evaluation, options) for a in list1.elements: if not any(sameQ(a, b) for b in list2.elements): return SymbolFalse return SymbolTrue - def eval_msg(self, e1, e2, evaluation, options={}): - "ContainsOnly[e1_, e2_, OptionsPattern[ContainsOnly]]" - + def eval_msg(self, e1, e2, evaluation, expression, options={}): + "expression:(ContainsOnly[e1_, e2_, OptionsPattern[]])" opts = ( options_to_rules(options) if len(options) <= 1 else [ListExpression(*options_to_rules(options))] ) - expr = Expression(SymbolContainsOnly, e1, e2, *opts) if not isinstance(e1, Symbol) and not e1.has_form("List", None): evaluation.message("ContainsOnly", "lsa", e1) - return self.check_options(expr, evaluation, options) + return expression if not isinstance(e2, Symbol) and not e2.has_form("List", None): evaluation.message("ContainsOnly", "lsa", e2) - return self.check_options(expr, evaluation, options) + return expression - return self.check_options(expr, evaluation, options) + return expression # TODO: ContainsAll, ContainsNone ContainsAny ContainsExactly diff --git a/mathics/builtin/makeboxes.py b/mathics/builtin/makeboxes.py index e75b9f0df..18a957e19 100644 --- a/mathics/builtin/makeboxes.py +++ b/mathics/builtin/makeboxes.py @@ -16,6 +16,7 @@ format_element, parenthesize, ) +from mathics.settings import SYSTEM_CHARACTER_ENCODING # TODO: Differently from the current implementation, MakeBoxes should only # accept as its format field the symbols in `$BoxForms`. This is something to @@ -96,7 +97,7 @@ class MakeBoxes(Builtin): 'MakeBoxes[Infix[head[elements], StringForm["~`1`~", head]], f]' ), "MakeBoxes[expr_]": "MakeBoxes[expr, StandardForm]", - "MakeBoxes[(form:StandardForm|TraditionalForm|OutputForm|TeXForm|" + "MakeBoxes[(form:StandardForm|TraditionalForm|TeXForm|" "MathMLForm)[expr_], StandardForm|TraditionalForm]": ("MakeBoxes[expr, form]"), "MakeBoxes[(form:StandardForm|OutputForm|MathMLForm|TeXForm)[expr_], OutputForm]": "MakeBoxes[expr, form]", "MakeBoxes[PrecedenceForm[expr_, prec_], f_]": "MakeBoxes[expr, f]", diff --git a/mathics/builtin/numbers/calculus.py b/mathics/builtin/numbers/calculus.py index 1f9c3bcc0..4ea600ca8 100644 --- a/mathics/builtin/numbers/calculus.py +++ b/mathics/builtin/numbers/calculus.py @@ -1410,9 +1410,9 @@ class NIntegrate(Builtin): messages.update( { "bdmtd": "The Method option should be a " - + "built-in method name in {`" - + "`, `".join(list(methods)) - + "`}. Using `Automatic`" + + r"built-in method name in {\`" + + r"\`, \`".join(list(methods)) + + r"\`}. Using \`Automatic\`." } ) diff --git a/mathics/core/atoms/numerics.py b/mathics/core/atoms/numerics.py index 3e4f78429..362831b54 100644 --- a/mathics/core/atoms/numerics.py +++ b/mathics/core/atoms/numerics.py @@ -522,11 +522,11 @@ def is_machine_precision(self) -> bool: return True def make_boxes(self, form): - from mathics.eval.makeboxes import NumberForm_to_String + from mathics.eval.makeboxes import numberform_to_boxes _number_form_options["_Form"] = form # passed to _NumberFormat n = 6 if form == "System`OutputForm" else None - num_str = NumberForm_to_String(self, n, None, None, _number_form_options) + num_str = numberform_to_boxes(self, n, None, None, _number_form_options) return num_str @property @@ -641,11 +641,11 @@ def is_zero(self) -> bool: return self.value.is_zero or False def make_boxes(self, form): - from mathics.eval.makeboxes import NumberForm_to_String + from mathics.eval.makeboxes import numberform_to_boxes _number_form_options["_Form"] = form # passed to _NumberFormat digits = dps(self.get_precision()) if form == "System`OutputForm" else None - return NumberForm_to_String(self, digits, None, None, _number_form_options) + return numberform_to_boxes(self, digits, None, None, _number_form_options) def round(self, d: Optional[int] = None) -> Union[MachineReal, "PrecisionReal"]: if d is None: diff --git a/mathics/core/parser/__init__.py b/mathics/core/parser/__init__.py index cdc9ffc6f..cb8178d4e 100644 --- a/mathics/core/parser/__init__.py +++ b/mathics/core/parser/__init__.py @@ -18,7 +18,7 @@ MathicsMultiLineFeeder, MathicsSingleLineFeeder, ) -from mathics.core.parser.operators import all_operator_names +from mathics.core.parser.operators import all_operator_names, operator_precedences from mathics.core.parser.util import parse, parse_builtin_rule __all__ = [ @@ -29,6 +29,7 @@ "MathicsSingleLineFeeder", "all_operator_names", "is_symbol_name", + "operator_precedences", "parse", "parse_builtin_rule", ] diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index 258ec92ef..5dba18221 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -196,6 +196,7 @@ SymbolNIntegrate = Symbol("System`NIntegrate") SymbolNValues = Symbol("System`NValues") SymbolNeeds = Symbol("System`Needs") +SymbolNonAssociative = Symbol("System`NonAssociative") SymbolNone = Symbol("System`None") SymbolNorm = Symbol("System`Norm") SymbolNormal = Symbol("System`Normal") @@ -235,9 +236,11 @@ SymbolPlus = Symbol("System`Plus") SymbolPoint = Symbol("System`Point") SymbolPolygon = Symbol("System`Polygon") +SymbolPostfix = Symbol("System`Postfix") SymbolPossibleZeroQ = Symbol("System`PossibleZeroQ") SymbolPower = Symbol("System`Power") SymbolPrecision = Symbol("System`Precision") +SymbolPrefix = Symbol("System`Prefix") SymbolPreserveImageOptions = Symbol("System`PreserveImageOptions") SymbolProlog = Symbol("System`Prolog") SymbolQuantity = Symbol("System`Quantity") diff --git a/mathics/doc/documentation/1-Manual.mdoc b/mathics/doc/documentation/1-Manual.mdoc index 5b717de11..88a28ceb5 100644 --- a/mathics/doc/documentation/1-Manual.mdoc +++ b/mathics/doc/documentation/1-Manual.mdoc @@ -901,9 +901,11 @@ This will even apply to 'TeXForm', because 'TeXForm' implies 'StandardForm': >> b // TeXForm = c -Except some other form is applied first: - >> b // OutputForm // TeXForm - = b + ## This test requires another round of changes in order to + ## pass: + ## Except some other form is applied first: + ## >> b // OutputForm // TeXForm + ## = \text{b} 'MakeBoxes' for another form: >> MakeBoxes[b, TeXForm] = "d"; @@ -934,12 +936,12 @@ For instance, you can override 'MakeBoxes' to format lists in a different way: >> {1, 2, 3} = {1, 2, 3} - #> {1, 2, 3} // TeXForm + >> {1, 2, 3} // TeXForm = \left[1 2 3\right] However, this will not be accepted as input to \Mathics anymore: >> [1 2 3] - : Expression cannot begin with "[1 2 3]" (line 1 of ""). + : Expression cannot begin with "[1 2 3]" (line 1 of ""). >> Clear[MakeBoxes] diff --git a/mathics/eval/makeboxes/__init__.py b/mathics/eval/makeboxes/__init__.py index 876ccccfd..4b839cd03 100644 --- a/mathics/eval/makeboxes/__init__.py +++ b/mathics/eval/makeboxes/__init__.py @@ -8,11 +8,16 @@ eval_generic_makeboxes, eval_makeboxes, eval_makeboxes_fullform, + eval_makeboxes_outputform, format_element, int_to_string_shorter_repr, to_boxes, ) -from mathics.eval.makeboxes.numberform import NumberForm_to_String, eval_baseform +from mathics.eval.makeboxes.numberform import ( + eval_baseform, + get_numberform_parameters, + numberform_to_boxes, +) from mathics.eval.makeboxes.operators import eval_infix, eval_postprefix from mathics.eval.makeboxes.outputforms import ( eval_mathmlform, @@ -26,7 +31,7 @@ ) __all__ = [ - "NumberForm_to_String", + "numberform_to_boxes", "StringLParen", "StringRParen", "_boxed_string", @@ -38,11 +43,13 @@ "eval_infix", "eval_makeboxes", "eval_makeboxes_fullform", + "eval_makeboxes_outputform", "eval_mathmlform", "eval_postprefix", "eval_tableform", "eval_texform", "format_element", + "get_numberform_parameters", "int_to_string_shorter_repr", "parenthesize", "render_input_form", diff --git a/mathics/eval/makeboxes/makeboxes.py b/mathics/eval/makeboxes/makeboxes.py index f436fea45..bc14d62ff 100644 --- a/mathics/eval/makeboxes/makeboxes.py +++ b/mathics/eval/makeboxes/makeboxes.py @@ -137,6 +137,20 @@ def int_to_string_shorter_repr(value: int, form: Symbol, max_digits=640): return String(value_str) +def eval_makeboxes_outputform(expr, evaluation, form, **kwargs): + """ + Build a 2D representation of the expression using only keyboard characters. + """ + from mathics.builtin.box.layout import PaneBox + from mathics.form.outputform import expression_to_outputform_text + + text_outputform = str( + expression_to_outputform_text(expr, evaluation, form, **kwargs) + ) + elem1 = PaneBox(String('"' + text_outputform + '"')) + return elem1 + + # TODO: evaluation is needed because `atom_to_boxes` uses it. Can we remove this # argument? def eval_makeboxes_fullform( @@ -271,10 +285,35 @@ def format_element( """ Applies formats associated to the expression, and then calls Makeboxes """ + # Halt any potential evaluation tracing while performing boxing. evaluation.is_boxing = True + while element.get_head() is form: + element = element.elements[0] + + # By now, eval_makeboxes_outputform is only used when we explicitly + # ask for MakeBoxes[OutputForm[expr], fmt] + # When it get ready, we can uncomment this. + # if form is SymbolOutputForm: + # return eval_makeboxes_outputform(element, evaluation, form, **kwargs) + + if element.has_form("FullForm", 1): + return eval_makeboxes_fullform(element.elements[0], evaluation) + + # In order to work like in WMA, `format_element` + # should evaluate `MakeBoxes[element//form, StandardForm]` + # Then, MakeBoxes[expr_, StandardForm], for any expr, + # should apply Format[...] rules, and then + # MakeBoxes[...] rules. These rules should be stored + # as FormatValues[...] + # As a first step in that direction, let's mimic this behaviour + # just for the case of OutputForm: + if element.has_form("OutputForm", 1): + return eval_makeboxes_outputform(element.elements[0], evaluation, form) + formatted_expr = do_format(element, evaluation, form) if formatted_expr is None: return None + result_box = eval_makeboxes(formatted_expr, evaluation, form) if isinstance(result_box, String): return result_box diff --git a/mathics/eval/makeboxes/numberform.py b/mathics/eval/makeboxes/numberform.py index 935a2c15a..7f9d55f81 100644 --- a/mathics/eval/makeboxes/numberform.py +++ b/mathics/eval/makeboxes/numberform.py @@ -3,12 +3,19 @@ """ from math import ceil -from typing import Optional, Tuple, Union +from typing import Any, Dict, Optional, Tuple, Union import mpmath -from mathics.core.atoms import Integer, MachineReal, PrecisionReal, Real, String -from mathics.core.element import BoxElementMixin +from mathics.core.atoms import ( + Integer, + Integer0, + MachineReal, + PrecisionReal, + Real, + String, +) +from mathics.core.element import BaseElement, BoxElementMixin from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.number import ( @@ -17,13 +24,29 @@ convert_base, dps, ) +from mathics.core.symbols import Symbol, SymbolNull from mathics.core.systemsymbols import ( + SymbolFullForm, SymbolMakeBoxes, SymbolOutputForm, SymbolSubscriptBox, ) from mathics.eval.makeboxes import to_boxes +DEFAULT_NUMBERFORM_OPTIONS = { + "DigitBlock": [0, 0], + "ExponentFunction": lambda x: (SymbolNull if abs(x.value) <= 5 else x), + "ExponentStep": 1, + "NumberFormat": lambda x: x, + "NumberMultiplier": "×", + "NumberPadding": ["", "0"], + "NumberPoint": ".", + "NumberSeparator": [",", ""], + "NumberSigns": ["-", ""], + "SignPadding": False, + "_Form": "System`FullForm", +} + def int_to_tuple_info(integer: Integer) -> Tuple[str, int, bool]: """ @@ -43,74 +66,255 @@ def int_to_tuple_info(integer: Integer) -> Tuple[str, int, bool]: return s, exponent, is_nonnegative -def real_to_tuple_info(real: Real, digits: Optional[int]) -> Tuple[str, int, bool]: +def real_to_tuple_info( + real: Real, digits: Optional[int] +) -> Tuple[str, int, bool, int, int]: """ Convert ``real`` to a tuple representing that value. The tuple consists of: * the string absolute value of ``integer`` with decimal point removed from the string; the position of the decimal point is determined by the exponent below, * the exponent, base 10, to be used, and * True if the value is nonnegative or False otherwise. + * Updated value of digits, according to the number precision. + * the decimal precision. If ``digits`` is None, we use the default precision. """ + binary_precision = real.get_precision() + precision = dps(binary_precision) + if digits is None: + digits = precision + 1 + else: + digits = min(digits, precision + 1) + if real.is_zero: s = "0" if real.is_machine_precision(): exponent = 0 else: - p = real.get_precision() - exponent = -dps(p) + exponent = -precision is_nonnegative = True - else: - if digits is None: - if real.is_machine_precision(): - value = real.value - s = repr(value) - else: - with mpmath.workprec(real.get_precision()): - value = real.to_mpmath() - s = mpmath.nstr(value, dps(real.get_precision()) + 1) + return s, exponent, is_nonnegative, digits, precision + + if digits is None: + if real.is_machine_precision(): + value = real.value + s = repr(value) else: - with mpmath.workprec(real.get_precision()): + with mpmath.workprec(binary_precision): value = real.to_mpmath() - s = mpmath.nstr(value, digits) + s = mpmath.nstr(value, precision + 1) + else: + with mpmath.workprec(binary_precision): + value = real.to_mpmath() + s = mpmath.nstr(value, digits) - # Set sign prefix. - if s[0] == "-": - assert value < 0 - is_nonnegative = False - s = s[1:] - else: - assert value >= 0 - is_nonnegative = True - # Set exponent. ``exponent`` is actual, ``pexp`` of ``NumberForm_to_string()`` is printed. - if "e" in s: - s, exponent = s.split("e") - exponent = int(exponent) - if len(s) > 1 and s[1] == ".": - # str(float) doesn't always include '.' if 'e' is present. - s = s[0] + s[2:].rstrip("0") + # Set sign prefix. + if s[0] == "-": + assert value < 0 + is_nonnegative = False + s = s[1:] + else: + assert value >= 0 + is_nonnegative = True + # Set exponent. ``exponent`` is actual, ``pexp`` of ``NumberForm_to_string()`` is printed. + if "e" in s: + s, exponent = s.split("e") + exponent = int(exponent) + if len(s) > 1 and s[1] == ".": + # str(float) doesn't always include '.' if 'e' is present. + s = s[0] + s[2:].rstrip("0") + else: + exponent = s.index(".") - 1 + s = s[: exponent + 1] + s[exponent + 2 :].rstrip("0") + + # Normalize exponent: remove leading '0's after the decimal point + # and adjust the exponent accordingly. + i = 0 + while i < len(s) and s[i] == "0": + i += 1 + exponent -= 1 + s = s[i:] + + # Add trailing zeros for precision reals. + if digits is not None and not real.is_machine_precision() and len(s) < digits: + s = s + "0" * (digits - len(s)) + return s, exponent, is_nonnegative, digits, precision + + +def eval_baseform( + expr: BaseElement, n: BaseElement, f: Symbol, evaluation: Evaluation +) -> BoxElementMixin: + """ + Evaluate MakeBoxes[BaseForm[expr_, n_], f_] + + Parameters + ---------- + expr : BaseElement + the expression. + n : BaseElement + the base. + f : Symbol + Form (StandardForm/TraditionalForm). + evaluation : Evaluation + Evaluation object used for messages. + + Returns + ------- + BoxElementMixin + A String or a box expression representing `expr` in base `n`. + + """ + try: + val, base = get_baseform_elements(expr, n, evaluation) + except ValueError: + return None + if base is None: + return to_boxes(Expression(SymbolMakeBoxes, expr, f), evaluation) + if f is SymbolOutputForm: + return to_boxes(String(f"{val}_{base}"), evaluation) + + return to_boxes( + Expression(SymbolSubscriptBox, String(val), String(base)), evaluation + ) + + +def get_baseform_elements( + expr: BaseElement, n: BaseElement, evaluation: Evaluation +) -> Dict[str, Any]: + """ + Collect the options for BaseForm expressions. + + Parameters + ---------- + expr : BaseElement + Expression to be formatted. + n : BaseElement + The base of the numeration. + evaluation : Evaluation + Evaluation object used for show messages. + + Raises + ------ + ValueError + If some of the parameters is not valid. + + Returns + ------- + Dict[str, Any] + A dictionary with the option values. + + """ + + if not isinstance(n, Integer): + evaluation.message("BaseForm", "intpm", expr, n) + raise ValueError + + base = n.value + if base <= 0: + evaluation.message("BaseForm", "intpm", expr, n) + raise ValueError + + if isinstance(expr, PrecisionReal): + x = expr.to_sympy() + p = int(ceil(expr.get_precision() / LOG2_10) + 1) + elif isinstance(expr, MachineReal): + x = expr.value + p = RECONSTRUCT_MACHINE_PRECISION_DIGITS + elif isinstance(expr, Integer): + x = expr.value + p = 0 + else: + return None, None + try: + return convert_base(x, base, p), base + except ValueError: + evaluation.message("BaseForm", "basf", n) + raise + + +def get_numberform_parameters( + full_expr, evaluation +) -> Tuple[BaseElement, BaseElement, Dict[str, Any]]: + """Collect the parameters of a NumberForm[...] expression. + Return a tuple with the expression, to be formatted, + the precision especification and a dictionary of options + with Python values + """ + # Pick the + num_form = full_expr.head + elements = full_expr.elements + form_name = num_form.get_name() + full_expr = Expression(SymbolFullForm, full_expr) + # This picks the builtin object used to do the option + # checks... + self = evaluation.definitions.builtin[form_name].builtin + default_options: [str, BaseElement] = evaluation.definitions.get_options(form_name) + options: Dict[str, BaseElement] = {} + py_options: Dict = {} + + if len(elements) == 0: + evaluation.message(form_name, "argm", num_form, Integer0) + raise ValueError + # Just one parameter. Silently return: + if len(elements) == 1: + py_options = self.check_and_convert_options(default_options, evaluation) + if py_options is None: + raise ValueError + + return elements[0], None, py_options + # expr is now the target expression: + expr, *elements = elements + # Collect options + pos = len(elements) + options.update(default_options) + for elem in elements[::-1]: + pos = pos - 1 + if elem.has_form(("Rule", "RuleDelayed"), 2): + key, val = elem.elements + if isinstance(key, Symbol): + key_name = key.get_name() + if key_name not in options: + evaluation.message(form_name, "optx", key, num_form, *elements) + raise ValueError + options[key_name] = val + else: + evaluation.message(form_name, "optx", key, num_form, *elements) + raise ValueError else: - exponent = s.index(".") - 1 - s = s[: exponent + 1] + s[exponent + 2 :].rstrip("0") - - # Normalize exponent: remove leading '0's after the decimal point - # and adjust the exponent accordingly. - i = 0 - while i < len(s) and s[i] == "0": - i += 1 - exponent -= 1 - s = s[i:] - - # Add trailing zeros for precision reals. - if digits is not None and not real.is_machine_precision() and len(s) < digits: - s = s + "0" * (digits - len(s)) - return s, exponent, is_nonnegative + break + + # To many non-option arguments + if pos > 0: + evaluation.message(form_name, "argct", num_form, Integer(len(elements) + 1)) + raise ValueError + # Check for validity of the values: + if pos == 0: + precision_parms = elements[0] + else: + precision_parms = None + + py_options = self.check_and_convert_options(options, evaluation) + + if isinstance(precision_parms, Integer): + val = precision_parms.value + if val <= 0: + evaluation.message(form_name, "iprf", precision_parms) + precision_parms = None + elif precision_parms.has_form("List", 2): + if any( + not isinstance(x, Integer) or x.value <= 0 for x in precision_parms.elements + ): + evaluation.message(form_name, "iprf", precision_parms) + precision_parms = None + else: + evaluation.message(form_name, "iprf", precision_parms) + precision_parms = None + + return expr, precision_parms, py_options -# FIXME: the return type should be a NumberForm, not a String. -# when this is fixed, rename the function. -def NumberForm_to_String( +def numberform_to_boxes( value: Union[Real, Integer], digits: Optional[int], digits_after_decimal_point: Optional[int], @@ -118,7 +322,7 @@ def NumberForm_to_String( options: dict, ) -> BoxElementMixin: """ - Converts a Real or Integer value to a String. + Converts a Real or Integer value to a String or a BoxExpression. ``digits`` is the number of digits of precision and ``digits_after_decimal_point`` is the number of digits after the @@ -133,121 +337,126 @@ def NumberForm_to_String( from the converted number, that is, otherwise the number may be padded on the right-hand side with zeros. """ - form = options["_Form"] - assert isinstance(digits, int) and digits > 0 or digits is None - assert digits_after_decimal_point is None or ( - isinstance(digits_after_decimal_point, int) and digits_after_decimal_point >= 0 - ) + # Ensure that all the options are valid options + for key, val in DEFAULT_NUMBERFORM_OPTIONS.items(): + if options.get(key, None) is None: + options[key] = val + + form = options["_Form"] + # Get information about `value` is_int = False if isinstance(value, Integer): assert digits is not None + precision = None s, exp, is_nonnegative = int_to_tuple_info(value) if digits_after_decimal_point is None: is_int = True elif isinstance(value, Real): - precision = dps(value.get_precision()) - if digits is None: - digits = precision + 1 - else: - digits = min(digits, precision + 1) - s, exp, is_nonnegative = real_to_tuple_info(value, digits) - if digits is None: - digits = len(s) + s, exp, is_nonnegative, digits, precision = real_to_tuple_info(value, digits) else: raise ValueError("Expected Real or Integer.") - assert isinstance(digits, int) and digits > 0 - - sign_prefix = options["NumberSigns"][1 if is_nonnegative else 0] - - # round exponent to ExponentStep - rexp = (exp // options["ExponentStep"]) * options["ExponentStep"] + options["_digits_after_decimal_point"] = digits_after_decimal_point - if is_int: - # integer never uses scientific notation - pexp = "" - else: - method = options["ExponentFunction"] - pexp = method(Integer(rexp)).get_int_value() - if pexp is not None: - exp -= pexp - pexp = str(pexp) - else: - pexp = "" + ( + left, + right, + exp, + pexp, + ) = _format_exponent(s, exp, is_int, evaluation, options) + left, right = _do_pre_paddings(left, right, form, exp, options) - # pad right with '0'. - if len(s) < exp + 1: - if evaluation is not None: - evaluation.message("NumberForm", "sigz") - # TODO NumberPadding? + digit_block = options["DigitBlock"] + number_sep = options["NumberSeparator"] + if digit_block[0]: + left = _add_digit_block_separators( + left, len(left) % digit_block[0], digit_block[0], number_sep[0] + ) + if digit_block[1]: + right = _add_digit_block_separators(right, 0, digit_block[1], number_sep[1]) + prefix, s = _do_padding( + ( + left, + right, + ), + digits, + is_nonnegative, + is_int, + options, + ) + s = _attach_precision(s, value, form, precision) - s = s + "0" * (1 + exp - len(s)) - # pad left with '0'. - if exp < 0: - s = "0" * (-exp) + s - exp = 0 + # PrintForms attach the prefix to the number. FullForm and $BoxForms + # put the prefix and the number in a RowBox: + if form not in ("System`StandardForm", "System`TraditionalForm", "System`FullForm"): + s = prefix + s + prefix = "" - # left and right of NumberPoint - left, right = s[: exp + 1], s[exp + 1 :] - - def _round(number, ndigits): - """ - python round() for integers but with correct rounding. - e.g. `_round(14225, -1)` is `14230` not `14220`. - """ - assert isinstance(ndigits, int) - assert ndigits < 0 - assert isinstance(number, int) - assert number >= 0 - number += 5 * int(10 ** -(1 + ndigits)) - number //= int(10**-ndigits) - return number + # build number + boxed_s = String(s) + if pexp: + # base + boxed_s = options["NumberFormat"]( + boxed_s, + String("10"), + String(pexp), + options, + ) + if prefix: + from mathics.builtin.box.layout import RowBox - # pad with NumberPadding - if digits_after_decimal_point is None: - if form != "System`OutputForm": - # Other forms strip trailing zeros: + boxed_s = RowBox(String(prefix), boxed_s) + return boxed_s - while len(right) > 0: - if right[-1] == "0": - right = right[:-1] - else: - break - else: - if len(right) < digits_after_decimal_point: - # pad right - right = ( - right - + (digits_after_decimal_point - len(right)) - * options["NumberPadding"][1] - ) - elif len(right) > digits_after_decimal_point: - # round right - tmp = int(left + right) - tmp = _round(tmp, digits_after_decimal_point - len(right)) - tmp = str(tmp) - left, right = tmp[: exp + 1], tmp[exp + 1 :] +def _add_digit_block_separators( + part: str, start: int, block_size: int, num_sep: str +) -> str: + """Add the digit block separator""" - def split_string(s, start, step): + def _split_string(s, start, step): if start > 0: yield s[:start] for i in range(start, len(s), step): yield s[i : i + step] - # insert NumberSeparator - digit_block = options["DigitBlock"] - if digit_block[0] != 0: - left = split_string(left, len(left) % digit_block[0], digit_block[0]) - left = options["NumberSeparator"][0].join(left) - if digit_block[1] != 0: - right = split_string(right, 0, digit_block[1]) - right = options["NumberSeparator"][1].join(right) + part = _split_string(part, start, block_size) + part = num_sep.join(part) + return part + + +def _attach_precision(s: str, value, form: str, precision: float) -> str: + """Add the precision mark if needed.""" + + if isinstance(value, MachineReal): + if form not in ("System`InputForm", "System`OutputForm"): + s = s + "`" + elif isinstance(value, PrecisionReal): + if form not in ("System`OutputForm"): + str_precision = str(precision) + if "." not in str_precision: + str_precision += "." + s = s + "`" + str_precision + return s + +def _do_padding( + parts: Tuple[str, str], + digits: int, + is_nonnegative: bool, + is_int: bool, + options: Dict[str, Any], +) -> Tuple[str, str]: + """ + Rebuild the prefix and magnitud + """ + left, right = parts left_padding = 0 - max_sign_len = max(len(options["NumberSigns"][0]), len(options["NumberSigns"][1])) + sign_prefix = options["NumberSigns"][1 if is_nonnegative else 0] + max_sign_len = max(len(ns) for ns in options["NumberSigns"]) + i = len(sign_prefix) + len(left) + len(right) - max_sign_len if i < digits: left_padding = digits - i @@ -265,82 +474,81 @@ def split_string(s, start, step): s = left else: s = left + options["NumberPoint"] + right + return prefix, s - # PrintForms attach the prefix to the number. FullForm and $BoxForms - # put the prefix and the number in a RowBox: - if form not in ("System`StandardForm", "System`TraditionalForm", "System`FullForm"): - s = prefix + s - prefix = "" - if isinstance(value, MachineReal): - if form not in ("System`InputForm", "System`OutputForm"): - s = s + "`" - elif isinstance(value, PrecisionReal): - if form not in ("System`OutputForm"): - str_precision = str(precision) - if "." not in str_precision: - str_precision += "." - s = s + "`" + str_precision +def _do_pre_paddings( + left: str, right: str, form: str, exp: int, options +) -> Tuple[str, str]: + # pad with NumberPadding + daadp = options["_digits_after_decimal_point"] + if daadp is None: + if form != "System`OutputForm": + # Other forms strip trailing zeros: - # build number - boxed_s = String(s) - if pexp: - # base - base = "10" - method = options["NumberFormat"] - boxed_base = String(base) - boxed_pexp = String(pexp) - boxed_s = method( - boxed_s, - boxed_base, - boxed_pexp, - options, - ) - if prefix: - from mathics.builtin.box.layout import RowBox + while len(right) > 0: + if right[-1] == "0": + right = right[:-1] + else: + break + else: + if len(right) < daadp: + # pad right + right = right + (daadp - len(right)) * options["NumberPadding"][1] + elif len(right) > daadp: + # round right + tmp = int(left + right) + tmp = _round(tmp, daadp - len(right)) + tmp = str(tmp) + left, right = tmp[: exp + 1], tmp[exp + 1 :] + return left, right - boxed_s = RowBox(String(prefix), boxed_s) - return boxed_s +def _format_exponent( + s: str, exp: int, is_int: bool, evaluation: Evaluation, options: Dict[str, Any] +) -> Tuple[str, str, int, str]: + # round exponent to ExponentStep + exponent_step = options["ExponentStep"] + rexp = (exp // exponent_step) * exponent_step -def get_baseform_elements(expr, n, evaluation: Evaluation): - if not isinstance(n, Integer): - evaluation.message("BaseForm", "intpm", expr, n) - raise ValueError + if is_int: + # integer never uses scientific notation + pexp = "" + else: + method = options["ExponentFunction"] + pexp = method(Integer(rexp)).get_int_value() + if pexp is not None: + exp -= pexp + pexp = str(pexp) + else: + pexp = "" - base = n.value - if base <= 0: - evaluation.message("BaseForm", "intpm", expr, n) - raise ValueError + # pad right with '0'. + if len(s) < exp + 1: + if evaluation is not None: + evaluation.message("NumberForm", "sigz") - if isinstance(expr, PrecisionReal): - x = expr.to_sympy() - p = int(ceil(expr.get_precision() / LOG2_10) + 1) - elif isinstance(expr, MachineReal): - x = expr.value - p = RECONSTRUCT_MACHINE_PRECISION_DIGITS - elif isinstance(expr, Integer): - x = expr.value - p = 0 - else: - return None, None - try: - return convert_base(x, base, p), base - except ValueError: - evaluation.message("BaseForm", "basf", n) - raise + # TODO NumberPadding instead 0? + s = s + "0" * (1 + exp - len(s)) + # pad left with '0'. + if exp < 0: + s = "0" * (-exp) + s + exp = 0 + # left and right of NumberPoint + sleft, sright = s[: exp + 1], s[exp + 1 :] + return sleft, sright, exp, pexp -def eval_baseform(self, expr, n, f, evaluation: Evaluation): - try: - val, base = get_baseform_elements(expr, n, evaluation) - except ValueError: - return None - if base is None: - return to_boxes(Expression(SymbolMakeBoxes, expr, f), evaluation) - if f is SymbolOutputForm: - return to_boxes(String("%s_%d" % (val, base)), evaluation) - return to_boxes( - Expression(SymbolSubscriptBox, String(val), String(base)), evaluation - ) +def _round(number, ndigits): + """ + python round() for integers but with correct rounding. + e.g. `_round(14225, -1)` is `14230` not `14220`. + """ + assert isinstance(ndigits, int) + assert ndigits < 0 + assert isinstance(number, int) + assert number >= 0 + number += 5 * int(10 ** -(1 + ndigits)) + number //= int(10**-ndigits) + return number diff --git a/mathics/eval/strings.py b/mathics/eval/strings.py index 96a73a2c4..ae57acebe 100644 --- a/mathics/eval/strings.py +++ b/mathics/eval/strings.py @@ -3,7 +3,8 @@ """ import re -from mathics.core.atoms import Integer1, Integer3, String +from mathics.builtin.box.layout import RowBox +from mathics.core.atoms import Integer, Integer0, Integer1, Integer3, String from mathics.core.convert.expression import to_mathics_list from mathics.core.convert.python import from_bool from mathics.core.convert.regex import to_regex @@ -139,3 +140,87 @@ def convert_rule(r): ) else: return self._find(py_strings, py_rules, py_n, flags, evaluation) + + +def safe_backquotes(string: str): + """Handle escaped backquotes.""" + # TODO: Fix in the scanner how escaped backslashes + # are parsed. + # "\\`" must be parsed as "\\`" in order this + # works properly, but the parser converts `\\` + # into `\`. + string = string.replace(r"\\", r"\[RawBackslash]") + string = string.replace(r"\`", r"\[RawBackquote]") + string = string.replace(r"\[RawBackslash]", r"\\") + return string + + +def eval_StringForm_MakeBoxes(strform, items, form, evaluation): + """MakeBoxes[StringForm[s_String, items___], form_]""" + + if not isinstance(strform, String): + raise _WrongFormattedExpression + + items = [format_element(item, evaluation, form) for item in items] + + curr_indx = 0 + strform_str = safe_backquotes(strform.value) + + parts = strform_str.split("`") + parts = [part.replace("\\[RawBackquote]", "`") for part in parts] + result = [String(parts[0])] + if len(parts) <= 1: + return result[0] + + quote_open = True + remaining = len(parts) - 1 + num_items = len(items) + for part in parts[1:]: + remaining -= 1 + # If quote_open, the part must be a placeholder + if quote_open: + # If not remaining, there is a not closed '`' + # character: + if not remaining: + evaluation.message("StringForm", "sfq", strform) + return strform.value + # part must be an index or an empty string. + # If is an empty string, pick the next element: + if part == "": + if curr_indx >= num_items: + evaluation.message( + "StringForm", + "sfr", + Integer(num_items + 1), + Integer(num_items), + strform, + ) + return strform.value + result.append(items[curr_indx]) + curr_indx += 1 + quote_open = False + continue + # Otherwise, must be a positive integer: + try: + indx = int(part) + except ValueError: + evaluation.message( + "StringForm", "sfr", Integer0, Integer(num_items), strform + ) + return strform.value + # indx must be greater than 0, and not greater than + # the number of items + if indx <= 0 or indx > len(items): + evaluation.message( + "StringForm", "sfr", Integer(indx), Integer(len(items)), strform + ) + return strform.value + result.append(items[indx - 1]) + curr_indx = indx + quote_open = False + continue + + result.append(String(part)) + quote_open = True + + return RowBox(ListExpression(*result)) diff --git a/mathics/form/inputform.py b/mathics/form/inputform.py index e76598780..c531436d5 100644 --- a/mathics/form/inputform.py +++ b/mathics/form/inputform.py @@ -25,21 +25,16 @@ """ -from typing import Callable, Dict, Final, FrozenSet, List, Optional, Tuple +from typing import Callable, Dict from mathics.core.atoms import Integer, String -from mathics.core.convert.op import operator_to_ascii, operator_to_unicode from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression -from mathics.core.parser.operators import OPERATOR_DATA, operator_to_string -from mathics.core.symbols import Atom, Symbol +from mathics.core.symbols import Atom from mathics.core.systemsymbols import ( - SymbolBlank, - SymbolBlankNullSequence, - SymbolBlankSequence, - SymbolInfix, SymbolInputForm, SymbolLeft, + SymbolNonAssociative, SymbolNone, SymbolRight, ) @@ -47,51 +42,19 @@ from mathics.eval.makeboxes.precedence import compare_precedence from mathics.settings import SYSTEM_CHARACTER_ENCODING -SymbolNonAssociative = Symbol("System`NonAssociative") -SymbolPostfix = Symbol("System`Postfix") -SymbolPrefix = Symbol("System`Prefix") - -PRECEDENCES: Final = OPERATOR_DATA.get("operator-precedences") -PRECEDENCE_BOX_GROUP: Final[int] = PRECEDENCES.get("BoxGroup", 670) -PRECEDENCE_PLUS: Final[int] = PRECEDENCES.get("Plus", 310) -PRECEDENCE_TIMES: Final[int] = PRECEDENCES.get("Times", 400) -PRECEDENCE_POWER: Final[int] = PRECEDENCES.get("Power", 590) +from .util import ( + ARITHMETIC_OPERATOR_STRINGS, + BLANKS_TO_STRINGS, + _WrongFormattedExpression, + collect_in_pre_post_arguments, + get_operator_str, + parenthesize, + square_bracket, +) EXPR_TO_INPUTFORM_TEXT_MAP: Dict[str, Callable] = {} -# This Exception if the expression should -# be processed by the default routine -class _WrongFormattedExpression(Exception): - pass - - -def get_operator_str(head, evaluation, **kwargs) -> str: - encoding = kwargs["encoding"] - if isinstance(head, String): - op_str = head.value - elif isinstance(head, Symbol): - op_str = head.short_name - else: - return render_input_form(head, evaluation, **kwargs) - - if encoding == "ASCII": - operator = operator_to_ascii.get(op_str, op_str) - else: - operator = operator_to_unicode.get(op_str, op_str) - return operator - - -def bracket(expr_str: str) -> str: - """Wrap `expr_str` with square braces""" - return f"[{expr_str}]" - - -def parenthesize(expr_str: str) -> str: - """Wrap `expr_str` with parenthesis""" - return f"({expr_str})" - - def register_inputform(head_name): def _register(func): EXPR_TO_INPUTFORM_TEXT_MAP[head_name] = func @@ -157,7 +120,7 @@ def _generic_to_inputform_text( while elements: result = result + comma + elements.pop(0) - return head + bracket(result) + return head + square_bracket(result) @register_inputform("System`List") @@ -176,81 +139,6 @@ def _list_expression_to_inputform_text( return result + "}" -def collect_in_pre_post_arguments( - expr: Expression, evaluation: Evaluation, **kwargs -) -> Tuple[list, str | List[str], int, Optional[Symbol]]: - """ - Determine operands, operator(s), precedence, and grouping - """ - # Processing the second argument, if it is there: - elements = expr.elements - # expr at least has to have one element - if len(elements) < 1: - raise _WrongFormattedExpression - - target = elements[0] - if isinstance(target, Atom): - raise _WrongFormattedExpression - - if not (0 <= len(elements) <= 4): - raise _WrongFormattedExpression - - head = expr.head - group = None - precedence = PRECEDENCE_BOX_GROUP - operands = list(target.elements) - - # Just one parameter: - if len(elements) == 1: - operator_spec = render_input_form(head, evaluation, **kwargs) - if head is SymbolInfix: - operator_spec = [ - f"{operator_to_string['Infix']}{operator_spec}{operator_to_string['Infix']}" - ] - elif head is SymbolPrefix: - operator_spec = f"{operator_spec}{operator_to_string['Prefix']}" - elif head is SymbolPostfix: - operator_spec = f"{operator_to_string['Postfix']}{operator_spec}" - return operands, operator_spec, precedence, group - - # At least two parameters: get the operator spec. - ops = elements[1] - if head is SymbolInfix: - # This is not the WMA behaviour, but the Mathics3 current implementation requires it: - ops = ops.elements if ops.has_form("List", None) else (ops,) - operator_spec = [get_operator_str(op, evaluation, **kwargs) for op in ops] - else: - operator_spec = get_operator_str(ops, evaluation, **kwargs) - - # At least three arguments: get the precedence - if len(elements) > 2: - if isinstance(elements[2], Integer): - precedence = elements[2].value - else: - raise _WrongFormattedExpression - - # Four arguments: get the grouping: - if len(elements) > 3: - group = elements[3] - if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): - raise _WrongFormattedExpression - if group is SymbolNone: - group = None - - return operands, operator_spec, precedence, group - - -ARITHMETIC_OPERATOR_STRINGS: Final[FrozenSet[str]] = frozenset( - [ - *operator_to_string["Divide"], - *operator_to_string["NonCommutativeMultiply"], - *operator_to_string["Power"], - *operator_to_string["Times"], - " ", - ] -) - - @register_inputform("System`Infix") def _infix_expression_to_inputform_text( expr: Expression, evaluation: Evaluation, **kwargs @@ -271,36 +159,34 @@ def _infix_expression_to_inputform_text( if len(operands) < 2: raise _WrongFormattedExpression - # Process the operands: - parenthesized = group in (None, SymbolRight, SymbolNonAssociative) - for index, operand in enumerate(operands): + # Process the first operand: + parenthesized = group in (SymbolNone, SymbolRight, SymbolNonAssociative) + operand = operands[0] + result = str(render_input_form(operand, evaluation, **kwargs)) + result = parenthesize(precedence, operand, result, parenthesized) + + if group in (SymbolLeft, SymbolRight): + parenthesized = not parenthesized + + # Process the rest of operands + num_ops = len(ops_lst) + for index, operand in enumerate(operands[1:]): + curr_op = ops_lst[index % num_ops] + # In OutputForm we always add the spaces, except for + # " " + if curr_op not in ARITHMETIC_OPERATOR_STRINGS: + curr_op = f" {curr_op} " + operand_txt = str(render_input_form(operand, evaluation, **kwargs)) - cmp_precedence = compare_precedence(operand, precedence) - if cmp_precedence is not None and ( - cmp_precedence == -1 or (cmp_precedence == 0 and parenthesized) - ): - operand_txt = parenthesize(operand_txt) - - if index == 0: - result = operand_txt - # After the first element, for lateral - # associativity, parenthesized is flipped: - if group in (SymbolLeft, SymbolRight): - parenthesized = not parenthesized - else: - num_ops = len(ops_lst) - curr_op = ops_lst[index % num_ops] - if curr_op not in ARITHMETIC_OPERATOR_STRINGS: - # In the tests, we add spaces just for + and -: - curr_op = f" {curr_op} " - - result = "".join( - ( - result, - curr_op, - operand_txt, - ) + operand_txt = parenthesize(precedence, operand, operand_txt, parenthesized) + + result = "".join( + ( + result, + curr_op, + operand_txt, ) + ) return result @@ -309,7 +195,7 @@ def _prefix_expression_to_inputform_text( expr: Expression, evaluation: Evaluation, **kwargs ) -> str: """ - Convert Prefix[...] into a InputForm string. + Convert Prefix[...] into a OutputForm string. """ kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) operands, op_head, precedence, group = collect_in_pre_post_arguments( @@ -320,10 +206,9 @@ def _prefix_expression_to_inputform_text( raise _WrongFormattedExpression operand = operands[0] kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) - cmp_precedence = compare_precedence(operand, precedence) target_txt = render_input_form(operand, evaluation, **kwargs) - if cmp_precedence is not None and cmp_precedence != -1: - target_txt = parenthesize(target_txt) + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) + target_txt = parenthesize(precedence, operand, target_txt, True) return op_head + target_txt @@ -332,7 +217,7 @@ def _postfix_expression_to_inputform_text( expr: Expression, evaluation: Evaluation, **kwargs ) -> str: """ - Convert Postfix[...] into a InputForm string. + Convert Postfix[...] into a OutputForm string. """ kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) operands, op_head, precedence, group = collect_in_pre_post_arguments( @@ -342,10 +227,9 @@ def _postfix_expression_to_inputform_text( if len(operands) != 1: raise _WrongFormattedExpression operand = operands[0] - cmp_precedence = compare_precedence(operand, precedence) target_txt = render_input_form(operand, evaluation, **kwargs) - if cmp_precedence is not None and cmp_precedence != -1: - target_txt = parenthesize(target_txt) + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) + target_txt = parenthesize(precedence, operand, target_txt, True) return target_txt + op_head @@ -361,13 +245,10 @@ def _blanks(expr: Expression, evaluation: Evaluation, **kwargs): else: elem = "" head = expr.head - if head is SymbolBlank: - return "_" + elem - elif head is SymbolBlankSequence: - return "__" + elem - elif head is SymbolBlankNullSequence: - return "___" + elem - return _generic_to_inputform_text(expr, evaluation, **kwargs) + try: + return BLANKS_TO_STRINGS[head] + elem + except KeyError: + return _generic_to_inputform_text(expr, evaluation, **kwargs) @register_inputform("System`Pattern") @@ -389,7 +270,7 @@ def _rule_to_inputform_text(expr, evaluation: Evaluation, **kwargs): if len(elements) != 2: return _generic_to_inputform_text(expr, evaluation, **kwargs) pat, rule = (render_input_form(elem, evaluation, **kwargs) for elem in elements) - + kwargs["_render_function"] = render_input_form op_str = get_operator_str(head, evaluation, **kwargs) # In WMA there are spaces between operators. return pat + f" {op_str} " + rule diff --git a/mathics/form/outputform.py b/mathics/form/outputform.py new file mode 100644 index 000000000..0e81f8727 --- /dev/null +++ b/mathics/form/outputform.py @@ -0,0 +1,873 @@ +""" +This module implements the "OutputForm" textual representation of expressions. + +OutputForm is two-dimensional keyboard-character-only output, suitable for CLI +and text terminals. +""" + +import re +from typing import Callable, Dict, List, Union + +from mathics.core.atoms import ( + Integer, + Integer0, + Integer1, + Integer2, + IntegerM1, + Rational, + Real, + String, +) +from mathics.core.element import BaseElement +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.symbols import Atom, Symbol, SymbolList, SymbolTimes +from mathics.core.systemsymbols import ( + SymbolDerivative, + SymbolGrid, + SymbolInfinity, + SymbolInfix, + SymbolLeft, + SymbolNonAssociative, + SymbolNone, + SymbolOutputForm, + SymbolPower, + SymbolRight, + SymbolStandardForm, + SymbolTableForm, + SymbolTraditionalForm, +) +from mathics.eval.makeboxes import compare_precedence, do_format # , format_element +from mathics.eval.makeboxes.numberform import get_baseform_elements +from mathics.eval.testing_expressions import expr_min +from mathics.settings import SYSTEM_CHARACTER_ENCODING + +from .util import ( + BLANKS_TO_STRINGS, + PRECEDENCE_PLUS, + PRECEDENCE_POWER, + PRECEDENCE_TIMES, + _WrongFormattedExpression, + collect_in_pre_post_arguments, + get_operator_str, + parenthesize, + process_options, + square_bracket, + text_cells_to_grid, +) + +EXPR_TO_OUTPUTFORM_TEXT_MAP: Dict[str, Callable] = {} +MULTI_NEWLINE_RE = re.compile(r"\n{2,}") + + +class IsNotGrid(Exception): + pass + + +class IsNot2DArray(Exception): + pass + + +def _default_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> str: + """ + Default representation of a function + """ + if isinstance(expr, Atom): + result = expr.atom_to_boxes(SymbolOutputForm, evaluation) + if isinstance(result, String): + return result.value + return result.boxes_to_text() + + expr_head = expr.head + head = expression_to_outputform_text(expr_head, evaluation, **kwargs) + comma = ", " + elements = [ + expression_to_outputform_text(elem, evaluation) for elem in expr.elements + ] + result = elements.pop(0) if elements else "" + while elements: + result = result + comma + elements.pop(0) + + form = kwargs.get("_Form", SymbolOutputForm) + if form is SymbolTraditionalForm: + return head + f"({result})" + return head + square_bracket(result) + + +def _divide(num, den, evaluation, **kwargs): + infix_form = Expression( + SymbolInfix, + ListExpression(num, den), + String("/"), + Integer(PRECEDENCE_TIMES), + SymbolLeft, + ) + return expression_to_outputform_text(infix_form, evaluation, **kwargs) + + +def _strip_1_parm_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> str: + if len(expr.elements) != 1: + raise _WrongFormattedExpression + return expression_to_outputform_text(expr.elements[0], evaluation, **kwargs) + + +def register_outputform(head_name): + def _register(func): + EXPR_TO_OUTPUTFORM_TEXT_MAP[head_name] = func + return func + + return _register + + +@register_outputform("System`BaseForm") +def _baseform_outputform(expr: Expression, evaluation: Evaluation, **kwargs): + elements = expr.elements + if len(elements) != 2: + evaluation.message("BaseForm", "argr", Integer(len(elements)), Integer2) + raise _WrongFormattedExpression + + number, base_expr = elements + try: + val, base = get_baseform_elements(number, base_expr, evaluation) + except ValueError: + raise _WrongFormattedExpression + + if base is None: + return expression_to_outputform_text(number, evaluation, **kwargs) + return val + "_" + str(base) + + +@register_outputform("System`Blank") +@register_outputform("System`BlankSequence") +@register_outputform("System`BlankNullSequence") +def blank_pattern(expr: Expression, evaluation: Evaluation, **kwargs): + elements = expr.elements + if len(elements) > 1: + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) + if elements: + elem = expression_to_outputform_text(elements[0], evaluation, **kwargs) + else: + elem = "" + head = expr.head + try: + return BLANKS_TO_STRINGS[head] + elem + except KeyError: + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) + + +@register_outputform("System`Derivative") +def derivative_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> str: + """Derivative operator""" + head = expr.get_head() + if head is SymbolDerivative: + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) + super_head = head.get_head() + if super_head is SymbolDerivative: + expr_elements = expr.elements + if len(expr_elements) != 1: + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) + function_head = expression_to_outputform_text( + expr_elements[0], evaluation, **kwargs + ) + derivatives = head.elements + if len(derivatives) == 1: + order_iv = derivatives[0] + if order_iv == Integer1: + return function_head + "'" + elif order_iv == Integer2: + return function_head + "''" + + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) + + # Full Function with arguments: delegate to the default conversion. + # It will call us again with the head + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) + + +@register_outputform("System`Divide") +def divide_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> str: + if len(expr.elements) != 2: + raise _WrongFormattedExpression + num, den = expr.elements + return _divide(num, den, evaluation, **kwargs) + + +def expression_to_outputform_text(expr: BaseElement, evaluation: Evaluation, **kwargs): + """ + Build a pretty-print text from an `Expression` + """ + format_expr: Expression = do_format(expr, evaluation, SymbolOutputForm) # type: ignore + + while format_expr.has_form("HoldForm", 1): # type: ignore + format_expr = format_expr.elements[0] + + if format_expr is None: + return "" + + lookup_name: str = format_expr.get_head().get_lookup_name() + try: + result = EXPR_TO_OUTPUTFORM_TEXT_MAP[lookup_name]( + format_expr, evaluation, **kwargs + ) + return result + except _WrongFormattedExpression: + # If the key is not present, or the execution fails for any reason, use + # the default + pass + except KeyError: + pass + return _default_expression_to_outputform_text(format_expr, evaluation, **kwargs) + + +@register_outputform("System`Graphics") +def graphics(expr: Expression, evaluation: Evaluation, **kwargs) -> str: + return "-Graphics-" + + +@register_outputform("System`Graphics3D") +def graphics3d(expr: Expression, evaluation: Evaluation, **kwargs) -> str: + return "-Graphics3D-" + + +@register_outputform("System`Grid") +def grid_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> str: + if len(expr.elements) == 0: + raise IsNotGrid + if len(expr.elements) > 1 and not expr.elements[1].has_form( + ["Rule", "RuleDelayed"], 2 + ): + raise IsNotGrid + if not expr.elements[0].has_form("List", None): + raise IsNotGrid + + elements = expr.elements[0].elements + rows = [] + for idx, item in enumerate(elements): + if item.has_form("List", None): + rows.append( + [ + expression_to_outputform_text(item_elem, evaluation, **kwargs) + for item_elem in item.elements + ] + ) + else: + rows.append(expression_to_outputform_text(item, evaluation, **kwargs)) + + return text_cells_to_grid(rows) + + +register_outputform("System`HoldForm")(_strip_1_parm_expression_to_outputform_text) + + +@register_outputform("System`FullForm") +@register_outputform("System`InputForm") +def other_forms(expr, evaluation, **kwargs): + from mathics.eval.makeboxes import format_element + + result = format_element(expr, evaluation, SymbolStandardForm, **kwargs) + if isinstance(result, String): + return result.value + return result.boxes_to_text() + + +@register_outputform("System`Image") +def image_outputform_text(expr: Expression, evaluation: Evaluation, **kwargs): + return "-Image-" + + +@register_outputform("System`Infix") +def _infix_outputform_text(expr: Expression, evaluation: Evaluation, **kwargs) -> str: + """ + Convert Infix[...] into a InputForm string. + """ + # In WMA, expressions associated to Infix operators are not + # formatted using this path: in some way, when an expression + # has a head that matches with a symbol associated to an infix + # operator, WMA builds its inputform without passing through + # its "Infix" form. + kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) + operands, ops_lst, precedence, group = collect_in_pre_post_arguments( + expr, evaluation, **kwargs + ) + # Infix needs at least two operands: + if len(operands) < 2: + raise _WrongFormattedExpression + + # Process the first operand: + parenthesized = group in (SymbolNone, SymbolRight, SymbolNonAssociative) + operand = operands[0] + result = str(expression_to_outputform_text(operand, evaluation, **kwargs)) + result = parenthesize(precedence, operand, result, parenthesized) + + if group in (SymbolLeft, SymbolRight): + parenthesized = not parenthesized + + # Process the rest of operands + num_ops = len(ops_lst) + for index, operand in enumerate(operands[1:]): + curr_op = ops_lst[index % num_ops] + # In OutputForm we always add the spaces, except for + # " " + if curr_op != " ": + curr_op = f" {curr_op} " + + operand_txt = str(expression_to_outputform_text(operand, evaluation, **kwargs)) + operand_txt = parenthesize(precedence, operand, operand_txt, parenthesized) + + result = "".join( + ( + result, + curr_op, + operand_txt, + ) + ) + return result + + +@register_outputform("System`InputForm") +def inputform(expr: Expression, evaluation: Evaluation, **kwargs): + from .inputform import render_input_form + + return render_input_form(expr, evaluation, **kwargs) + + +@register_outputform("System`Integer") +def integer_expression_to_outputform_text(n: Integer, evaluation: Evaluation, **kwargs): + return str(n.value) + + +@register_outputform("System`List") +def list_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> str: + elements = expr.elements + if not elements: + return "{}" + + result, *rest_elems = ( + expression_to_outputform_text(elem, evaluation, **kwargs) + for elem in expr.elements + ) + comma_tb = ", " + for next_elem in rest_elems: + result = result + comma_tb + next_elem + return "{" + result + "}" + + +@register_outputform("System`MathMLForm") +def mathmlform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> str: + # boxes = format_element(expr.elements[0], evaluation) + boxes = Expression( + Symbol("System`MakeBoxes"), expr.elements[0], SymbolTraditionalForm + ).evaluate(evaluation) + return boxes.boxes_to_mathml() # type: ignore[union-attr] + + +@register_outputform("System`MatrixForm") +def matrixform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> str: + # return parenthesize(tableform_expression_to_outputform_text(expr, evaluation, **kwargs)) + return tableform_expression_to_outputform_text(expr, evaluation, **kwargs) + + +@register_outputform("System`MessageName") +def message_name_outputform(expr: Expression, evaluation: Evaluation, **kwargs): + elements = expr.elements + if len(elements) != 2: + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) + symb, msg = elements + if not (isinstance(symb, Symbol) and isinstance(msg, String)): + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) + symbol_name = evaluation.definitions.shorten_name(symb.get_name()) + return f"{symbol_name}::{msg.value}" + + +@register_outputform("System`OutputForm") +def outputform(expr: Expression, evaluation: Evaluation, **kwargs): + elements = expr.elements + if len(elements) != 1: + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) + return expression_to_outputform_text(elements[0], evaluation, **kwargs) + + +@register_outputform("System`Part") +def part(expr: Expression, evaluation: Evaluation, **kwargs): + elements = expr.elements + if len(elements) == 0: + raise _WrongFormattedExpression + elements_fmt = [ + expression_to_outputform_text(elem, evaluation, **kwargs) for elem in elements + ] + if len(elements_fmt) == 1: + return elements_fmt[0] + result = elements_fmt[0] + args = ", ".join(elements_fmt[1:]) + return f"{result}[[{args}]]" + + +@register_outputform("System`Pattern") +def pattern(expr: Expression, evaluation: Evaluation, **kwargs): + elements = expr.elements + if len(elements) != 2: + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) + name, pat = ( + expression_to_outputform_text(elem, evaluation, **kwargs) for elem in elements + ) + return name + pat + + +@register_outputform("System`Plus") +def plus_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + elements = expr.elements + result = "" + for i, term in enumerate(elements): + if term.has_form("Times", None): + # If the first element is -1, remove it and use + # a minus sign. Otherwise, if negative, do not add a sign. + first = term.elements[0] + if isinstance(first, Integer): + if first.value == -1: + result = ( + result + + " - " + + expression_to_outputform_text( + Expression(SymbolTimes, *term.elements[1:]), + evaluation, + **kwargs, + ) + ) + continue + elif first.value < 0: + result = ( + result + + " " + + expression_to_outputform_text(term, evaluation, **kwargs) + ) + continue + elif isinstance(first, Real): + if first.value < 0: + result = ( + result + + " " + + expression_to_outputform_text(term, evaluation, **kwargs) + ) + continue + result = ( + result + + " + " + + expression_to_outputform_text(term, evaluation, **kwargs) + ) + ## TODO: handle complex numbers? + else: + elem_txt = expression_to_outputform_text(term, evaluation, **kwargs) + if (compare_precedence(term, PRECEDENCE_PLUS) or -1) < 0: + elem_txt = parenthesize(elem_txt) + result = result + " + " + elem_txt + elif i == 0 or ( + (isinstance(term, Integer) and term.value < 0) + or (isinstance(term, Real) and term.value < 0) + ): + result = result + elem_txt + else: + result = ( + result + + " + " + + expression_to_outputform_text(term, evaluation, **kwargs) + ) + return result + + +@register_outputform("System`Power") +def power_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +): + if len(expr.elements) != 2: + raise _WrongFormattedExpression + + infix_form = Expression( + SymbolInfix, + ListExpression(*(expr.elements)), + String("^"), + Integer(PRECEDENCE_POWER), + SymbolRight, + ) + return expression_to_outputform_text(infix_form, evaluation, **kwargs) + + +@register_outputform("System`PrecedenceForm") +def precedenceform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> str: + if len(expr.elements) == 2: + return expression_to_outputform_text(expr.elements[0], evaluation, **kwargs) + raise _WrongFormattedExpression + + +@register_outputform("System`Prefix") +def _prefix_output_text(expr: Expression, evaluation: Evaluation, **kwargs) -> str: + """ + Convert Prefix[...] into a InputForm string. + """ + kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) + operands, op_head, precedence, group = collect_in_pre_post_arguments( + expr, evaluation, **kwargs + ) + # Prefix works with just one operand: + if len(operands) != 1: + raise _WrongFormattedExpression + operand = operands[0] + kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) + target_txt = expression_to_outputform_text(operand, evaluation, **kwargs) + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) + target_txt = parenthesize(precedence, operand, target_txt, True) + return op_head + target_txt + + +@register_outputform("System`Postfix") +def _postfix_output_text(expr: Expression, evaluation: Evaluation, **kwargs) -> str: + """ + Convert Postfix[...] into a InputForm string. + """ + kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) + operands, op_head, precedence, group = collect_in_pre_post_arguments( + expr, evaluation, **kwargs + ) + # Prefix works with just one operand: + if len(operands) != 1: + raise _WrongFormattedExpression + operand = operands[0] + target_txt = expression_to_outputform_text(operand, evaluation, **kwargs) + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) + target_txt = parenthesize(precedence, operand, target_txt, True) + return target_txt + op_head + + +@register_outputform("System`Rational") +def rational_expression_to_outputform_text( + n: Union[Rational, Expression], evaluation: Evaluation, **kwargs +): + if n.has_form("Rational", 2): + num, den = n.elements # type: ignore[union-attr] + else: + num, den = n.numerator(), n.denominator() # type: ignore[union-attr] + return _divide(num, den, evaluation, **kwargs) + + +@register_outputform("System`Real") +def real_expression_to_outputform_text(n: Real, evaluation: Evaluation, **kwargs): + str_n = n.make_boxes("System`OutputForm").boxes_to_text() # type: ignore[attr-defined] + return str(str_n) + + +@register_outputform("System`Row") +def row_to_outputform_text(expr, evaluation: Evaluation, **kwargs): + """Row[{...}]""" + elements = expr.elements[0].elements + return "".join( + expression_to_outputform_text(elem, evaluation, **kwargs) for elem in elements + ) + + +@register_outputform("System`Rule") +@register_outputform("System`RuleDelayed") +def rule_to_outputform_text(expr, evaluation: Evaluation, **kwargs): + """Rule|RuleDelayed[{...}]""" + head = expr.head + elements = expr.elements + kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) + if len(elements) != 2: + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) + pat, rule = ( + expression_to_outputform_text(elem, evaluation, **kwargs) for elem in elements + ) + kwargs["_render_function"] = expression_to_outputform_text + op_str = get_operator_str(head, evaluation, **kwargs) + return f"{pat} {op_str} {rule}" + + +@register_outputform("System`SequenceForm") +def sequenceform_to_outputform_text(expr, evaluation: Evaluation, **kwargs): + """Row[{...}]""" + elements = expr.elements + return "".join( + expression_to_outputform_text(elem, evaluation, **kwargs) for elem in elements + ) + + +@register_outputform("System`Slot") +def _slot_outputform_text(expr: Expression, evaluation: Evaluation, **kwargs): + elements = expr.elements + if len(elements) != 1: + raise _WrongFormattedExpression + slot = elements[0] + if isinstance(slot, Integer): + slot_value = slot.value + if slot_value < 0: + raise _WrongFormattedExpression + return f"#{slot_value}" + if isinstance(slot, String): + return f"#{slot.value}" + raise _WrongFormattedExpression + + +@register_outputform("System`SlotSequence") +def _slotsequence_outputform_text(expr: Expression, evaluation: Evaluation, **kwargs): + elements = expr.elements + if len(elements) != 1: + raise _WrongFormattedExpression + slot = elements[0] + if isinstance(slot, Integer): + slot_value = slot.value + if slot_value < 0: + raise _WrongFormattedExpression + return f"##{slot_value}" + if isinstance(slot, String): + return f"##{slot.value}" + raise _WrongFormattedExpression + + +@register_outputform("System`String") +def string_expression_to_outputform_text( + expr: String, evaluation: Evaluation, **kwargs +) -> str: + # lines = expr.value.split("\n") + # max_len = max([len(line) for line in lines]) + # lines = [line + (max_len - len(line)) * " " for line in lines] + # return "\n".join(lines) + return expr.value + + +@register_outputform("System`StringForm") +def stringform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> str: + strform = expr.elements[0] + if not isinstance(strform, String): + raise _WrongFormattedExpression + + items = list( + expression_to_outputform_text(item, evaluation, **kwargs) + for item in expr.elements[1:] + ) + + curr_indx = 0 + parts = strform.value.split("`") + result = str(parts[0]) + if len(parts) <= 1: + return result + + quote_open = True + remaining = len(parts) - 1 + num_items = len(items) + for part in parts[1:]: + remaining -= 1 + # If quote_open, the part must be a placeholder + if quote_open: + # If not remaining, there is a not closed '`' + # character: + if not remaining: + evaluation.message("StringForm", "sfq", strform) + return strform.value + # part must be an index or an empty string. + # If is an empty string, pick the next element: + if part == "": + if curr_indx >= num_items: + evaluation.message( + "StringForm", + "sfr", + Integer(num_items + 1), + Integer(num_items), + strform, + ) + return strform.value + result = result + items[curr_indx] + curr_indx += 1 + quote_open = False + continue + # Otherwise, must be a positive integer: + try: + indx = int(part) + except ValueError: + evaluation.message( + "StringForm", "sfr", Integer0, Integer(num_items), strform + ) + return strform.value + # indx must be greater than 0, and not greater than + # the number of items + if indx <= 0 or indx > len(items): + evaluation.message( + "StringForm", "sfr", Integer(indx), Integer(len(items)), strform + ) + return strform.value + result = result + items[indx - 1] + curr_indx = indx + quote_open = False + continue + + result = result + part + quote_open = True + + return result + + +@register_outputform("System`Style") +def style_to_outputform_text(expr: String, evaluation: Evaluation, **kwargs) -> str: + elements = expr.elements + if not elements: + raise _WrongFormattedExpression + return expression_to_outputform_text(elements[0], evaluation, **kwargs) + + +@register_outputform("System`Symbol") +def symbol_expression_to_outputform_text( + symb: Symbol, evaluation: Evaluation, **kwargs +): + return evaluation.definitions.shorten_name(symb.name) + + +@register_outputform("System`TableForm") +def tableform_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> str: + from mathics.builtin.tensors import get_dimensions + + elements = expr.elements + + if len(elements) == 0: + raise _WrongFormattedExpression + + table, *opts = elements + dims = len(get_dimensions(table, head=SymbolList)) + process_options(kwargs, opts) + depth = expr_min((Integer(dims), kwargs.pop("TableDepth", SymbolInfinity))).value + if depth is None: + evaluation.message(self.get_name(), "int") + raise _WrongFormattedExpression + if depth <= 0: + return expression_to_outputform_text(table, evaluation, **kwargs) + if depth == 1: + return text_cells_to_grid( + [ + [expression_to_outputform_text(elem, evaluation, **kwargs)] + for elem in table.elements + ] + ) + kwargs["TableDepth"] = Integer(depth - 2) + + def transform_item(item): + if depth > 2: + return tableform_expression_to_outputform_text( + Expression(SymbolTableForm, item), evaluation, **kwargs + ) + else: + return expression_to_outputform_text(item, evaluation, **kwargs) + + grid_array = [[transform_item(elem) for elem in row] for row in table.elements] + return text_cells_to_grid(grid_array) + + +@register_outputform("System`TeXForm") +def _texform_outputform(expr, evaluation, **kwargs): + boxes = Expression( + Symbol("System`MakeBoxes"), expr.elements[0], SymbolTraditionalForm + ).evaluate(evaluation) + try: + tex = boxes.boxes_to_tex(evaluation=evaluation) # type: ignore[union-attr] + tex = MULTI_NEWLINE_RE.sub("\n", tex) + tex = tex.replace(" \uF74c", " \\, d") # tmp hack for Integrate + return tex + except BoxError: + evaluation.message( + "General", + "notboxes", + Expression(SymbolFullForm, boxes).evaluate(evaluation), + ) + raise _WrongFormattedExpression + + +@register_outputform("System`Times") +def times_expression_to_outputform_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> str: + elements = expr.elements + if len(elements) < 2: + return _default_expression_to_outputform_text(expr, evaluation, **kwargs) + num: List[BaseElement] = [] + den: List[BaseElement] = [] + # First, split factors with integer, negative powers: + for factor in elements: + if factor.has_form("Power", 2): + base, exponent = factor.elements + if isinstance(exponent, Integer): + if exponent.value == -1: + den.append(base) + continue + elif exponent.value < 0: + den.append(Expression(SymbolPower, base, Integer(-exponent.value))) + continue + elif isinstance(factor, Rational): + num.append(factor.numerator()) + den.append(factor.denominator()) + continue + elif factor.has_form("Rational", 2): + elem_elements = factor.elements + num.append(elem_elements[0]) + den.append(elem_elements[1]) + continue + + num.append(factor) + + # If there are integer, negative powers, process as a fraction: + if den: + den_expr = den[0] if len(den) == 1 else Expression(SymbolTimes, *den) + num_expr = ( + Expression(SymbolTimes, *num) + if len(num) > 1 + else num[0] + if len(num) == 1 + else Integer1 + ) + return _divide(num_expr, den_expr, evaluation, **kwargs) + + # there are no integer negative powers: + if len(num) == 1: + return expression_to_outputform_text(num[0], evaluation, **kwargs) + + prefactor = 1 + result: str = "" + for i, factor in enumerate(num): + if factor is IntegerM1: + prefactor *= -1 + continue + if isinstance(factor, Integer): + prefactor *= -1 + factor = Integer(-factor.value) + + factor_txt = expression_to_outputform_text(factor, evaluation, **kwargs) + if compare_precedence(factor, PRECEDENCE_TIMES): + factor_txt = parenthesize(factor_txt) + if i == 0: + result = factor_txt + else: + result = result + " " + factor_txt + if result == "": + result = "1" + if prefactor == -1: + result = "-" + result + return result diff --git a/mathics/form/util.py b/mathics/form/util.py new file mode 100644 index 000000000..7e7c24bff --- /dev/null +++ b/mathics/form/util.py @@ -0,0 +1,258 @@ +""" + +Common routines and objects used in rendering PrintForms. + +""" +from typing import Final, FrozenSet, List, Optional, Tuple + +from mathics.core.atoms import Integer, String +from mathics.core.convert.op import operator_to_ascii, operator_to_unicode +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.parser.operators import OPERATOR_DATA, operator_to_string +from mathics.core.symbols import Atom, Symbol +from mathics.core.systemsymbols import ( + SymbolBlank, + SymbolBlankNullSequence, + SymbolBlankSequence, + SymbolInfix, + SymbolLeft, + SymbolNonAssociative, + SymbolNone, + SymbolPostfix, + SymbolPrefix, + SymbolRight, +) +from mathics.eval.makeboxes import compare_precedence + + +# This Exception if the expression should +# be processed by the default routine +class _WrongFormattedExpression(Exception): + pass + + +ARITHMETIC_OPERATOR_STRINGS: Final[FrozenSet[str]] = frozenset( + [ + *operator_to_string["Divide"], + *operator_to_string["NonCommutativeMultiply"], + *operator_to_string["Power"], + *operator_to_string["Times"], + " ", + ] +) + +PRECEDENCES: Final = OPERATOR_DATA.get("operator-precedences") +PRECEDENCE_BOX_GROUP: Final[int] = PRECEDENCES.get("BoxGroup", 670) +PRECEDENCE_PLUS: Final[int] = PRECEDENCES.get("Plus", 310) +PRECEDENCE_TIMES: Final[int] = PRECEDENCES.get("Times", 400) +PRECEDENCE_POWER: Final[int] = PRECEDENCES.get("Power", 590) + + +BLANKS_TO_STRINGS = { + SymbolBlank: "_", + SymbolBlankSequence: "__", + SymbolBlankNullSequence: "___", +} + + +def collect_in_pre_post_arguments( + expr: Expression, evaluation: Evaluation, **kwargs +) -> Tuple[list, str | List[str], int, Optional[Symbol]]: + """ + Determine operands, operator(s), precedence, and grouping + """ + # Processing the second argument, if it is there: + elements = expr.elements + # expr at least has to have one element + if len(elements) < 1: + raise _WrongFormattedExpression + + target = elements[0] + if isinstance(target, Atom): + raise _WrongFormattedExpression + + if not (0 <= len(elements) <= 4): + raise _WrongFormattedExpression + + head = expr.head + group = None + precedence = PRECEDENCE_BOX_GROUP + operands = list(target.elements) + + # Just one parameter: + if len(elements) == 1: + render_function = kwargs["_render_function"] + operator_spec = render_function(head, evaluation, **kwargs) + if head is SymbolInfix: + operator_spec = [ + f"{operator_to_string['Infix']}{operator_spec}{operator_to_string['Infix']}" + ] + elif head is SymbolPrefix: + operator_spec = f"{operator_spec}{operator_to_string['Prefix']}" + elif head is SymbolPostfix: + operator_spec = f"{operator_to_string['Postfix']}{operator_spec}" + return operands, operator_spec, precedence, group + + # At least two parameters: get the operator spec. + ops = elements[1] + if head is SymbolInfix: + # This is not the WMA behaviour, but the Mathics3 current implementation requires it: + ops = ops.elements if ops.has_form("List", None) else (ops,) + operator_spec = [get_operator_str(op, evaluation, **kwargs) for op in ops] + else: + operator_spec = get_operator_str(ops, evaluation, **kwargs) + + # At least three arguments: get the precedence + if len(elements) > 2: + if isinstance(elements[2], Integer): + precedence = elements[2].value + else: + raise _WrongFormattedExpression + + # Four arguments: get the grouping: + if len(elements) > 3: + group = elements[3] + if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): + raise _WrongFormattedExpression + if group is SymbolNone: + group = None + + return operands, operator_spec, precedence, group + + +def get_operator_str(head, evaluation, **kwargs) -> str: + encoding = kwargs["encoding"] + if isinstance(head, String): + op_str = head.value + elif isinstance(head, Symbol): + op_str = head.short_name + else: + render_function = kwargs["_render_function"] + return render_function(head, evaluation, **kwargs) + + if encoding == "ASCII": + operator = operator_to_ascii.get(op_str, op_str) + else: + operator = operator_to_unicode.get(op_str, op_str) + return operator + + +def parenthesize( + precedence: Optional[int], + element: Expression, + element_str, + when_equal: bool, +) -> Expression: + """ + "Add parenthesis to ``element_str`` according to the precedence of + ``element``. + + If when_equal is True, parentheses will be added if the two + precedence values are equal. + """ + cmp = compare_precedence(element, precedence) + if cmp is not None and (cmp == -1 or cmp == 0 and when_equal): + return f"({element_str})" + return element_str + + +def process_options(opts: dict, rules: Tuple): + for opt in rules: + if not opt.has_form(["Rule", "RuleDelayed"], 2): + raise _WrongFormattedExpression + opt_symb, opt_val = opt.elements + if isinstance(opt_symb, Symbol): + opts[opt_symb.get_name(short=True)] = opt_val + elif isinstance(opt_symb, String): + opts[opt_symb.value] = opt_val + else: + raise _WrongFormattedExpression + return + + +def square_bracket(expr_str: str) -> str: + """Wrap `expr_str` with square brackets""" + return f"[{expr_str}]" + + +def text_cells_to_grid(cells: List, **kwargs): + """ + Build from a tabulated grid from a list or + a list of lists of elements. + """ + if not cells: + return "" + cells = [[row] if isinstance(row, str) else row for row in cells] + for row in cells: + assert all(isinstance(field, str) for field in row) + + def normalize_rows(rows): + """ + Split each field in each row in a list of lines, and then + make that the number of lines on each field inside the same + row are the same. + """ + rows = [[field.split("\n") for field in row] for row in rows] + max_cols = max(len(row) for row in rows) + new_rows = [] + heights = [] + # TODO: implement vertical aligments + for row in rows: + max_height = max(len(field) for field in row) + new_row = [] + for field in row: + height = len(field) + if height < max_height: + field = field + [""] * (max_height - height) + new_row.append(field) + num_cols = len(new_row) + if num_cols < max_cols: + blank_field = [""] * max_height + new_row.extend([blank_field] * (max_cols - num_cols)) + new_rows.append(new_row) + heights.append(max_height) + return new_rows, heights + + def normalize_cols(rows): + """ + Ensure that all the lines of fields on the same line + have the same width. + """ + # TODO: implement horizontal alignments + col_widths = [0] * len(rows[0]) + for row in rows: + for col, cell in enumerate(row): + for line in cell: + col_widths[col] = max(col_widths[col], len(line)) + rows = [ + [ + [line.ljust(col_widths[col]) for line in cell] + for col, cell in enumerate(row) + ] + for row in rows + ] + return rows, col_widths + + cells, heights = normalize_rows(cells) + cells, col_widths = normalize_cols(cells) + row_sep = "\n\n" + col_sep = " " + result = "" + for row_idx, row in enumerate(cells): + if row_idx != 0: + result += row_sep + new_lines = [""] * heights[row_idx] + for field_no, field in enumerate(row): + for l_no, line in enumerate(field): + if field_no: + new_lines[l_no] += col_sep + line + else: + new_lines[l_no] += line + + # TODO: Remove me at the end... + new_lines = [line.rstrip() for line in new_lines] + new_text_row = "\n".join(new_lines) + result = result + new_text_row + + return result + "\n" diff --git a/test/builtin/list/test_list.py b/test/builtin/list/test_list.py index ddbcff8ff..dc6c07ac5 100644 --- a/test/builtin/list/test_list.py +++ b/test/builtin/list/test_list.py @@ -172,10 +172,7 @@ def test_rearrange_private_doctests( ), ( "ContainsOnly[{c, a}, {a, b, c}, IgnoreCase -> True]", - ( - "Unknown option IgnoreCase -> True in ContainsOnly.", - "Unknown option IgnoreCase in .", - ), + ("Unknown option IgnoreCase -> True in ContainsOnly.",), "True", None, ), diff --git a/test/builtin/numbers/test_nintegrate.py b/test/builtin/numbers/test_nintegrate.py index 8e9dee43d..369a5700d 100644 --- a/test/builtin/numbers/test_nintegrate.py +++ b/test/builtin/numbers/test_nintegrate.py @@ -46,7 +46,7 @@ "1.", None, [ - r"The Method option should be a built-in method name in {`Automatic`, `Internal`, `Simpson`, `NQuadrature`, `Quadrature`}. Using `Automatic`" + r"The Method option should be a built-in method name in {`Automatic`, `Internal`, `Simpson`, `NQuadrature`, `Quadrature`}. Using `Automatic`." ], ), ], @@ -62,7 +62,7 @@ "1.", None, [ - r"The Method option should be a built-in method name in {`Automatic`, `Internal`, `Simpson`}. Using `Automatic`" + r"The Method option should be a built-in method name in {`Automatic`, `Internal`, `Simpson`}. Using `Automatic`." ], ), ] diff --git a/test/builtin/test_forms.py b/test/builtin/test_forms.py index 25a1d6d88..7bc9e5f59 100644 --- a/test/builtin/test_forms.py +++ b/test/builtin/test_forms.py @@ -147,32 +147,32 @@ def test_makeboxes_form(expr, form, head, subhead): ( "Value for option DigitBlock should be a positive integer, Infinity, or a pair of positive integers.", ), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ( "NumberForm[1.2345, 3, DigitBlock -> x]", ( "Value for option DigitBlock should be a positive integer, Infinity, or a pair of positive integers.", ), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ( "NumberForm[1.2345, 3, DigitBlock -> {x, 3}]", ( "Value for option DigitBlock should be a positive integer, Infinity, or a pair of positive integers.", ), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ( "NumberForm[1.2345, 3, DigitBlock -> {5, -3}]", ( "Value for option DigitBlock should be a positive integer, Infinity, or a pair of positive integers.", ), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ## ExponentFunction ( @@ -213,14 +213,14 @@ def test_makeboxes_form(expr, form, head, subhead): ( "NumberForm[1.2345, 3, ExponentStep -> x]", ("Value of option ExponentStep -> x is not a positive integer.",), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ( "NumberForm[1.2345, 3, ExponentStep -> 0]", ("Value of option ExponentStep -> 0 is not a positive integer.",), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ( "NumberForm[y, 10, ExponentStep -> 6]", @@ -239,8 +239,8 @@ def test_makeboxes_form(expr, form, head, subhead): ( "NumberForm[1.2345, 3, NumberMultiplier -> 0]", ("Value for option NumberMultiplier -> 0 is expected to be a string.",), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ( 'NumberForm[N[10^ 7 Pi], 15, NumberMultiplier -> "*"]', @@ -253,8 +253,8 @@ def test_makeboxes_form(expr, form, head, subhead): ( "NumberForm[1.2345, 3, NumberPoint -> 0]", ("Value for option NumberPoint -> 0 is expected to be a string.",), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ## NumberPadding ("NumberForm[1.41, {10, 5}]", None, "1.41000", None), @@ -281,8 +281,8 @@ def test_makeboxes_form(expr, form, head, subhead): ( "Value for option NumberPadding -> 0 should be a string or a pair of strings.", ), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ( 'NumberForm[1.41, 10, NumberPadding -> {"X", "Y"}, NumberSigns -> {"-------------", ""}]', @@ -326,8 +326,8 @@ def test_makeboxes_form(expr, form, head, subhead): ( "Value for option NumberSeparator -> 0 should be a string or a pair of strings.", ), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ## NumberSigns ('NumberForm[1.2345, 5, NumberSigns -> {"-", "+"}]', None, "+1.2345", None), @@ -337,8 +337,8 @@ def test_makeboxes_form(expr, form, head, subhead): ( "Value for option NumberSigns -> 0 should be a pair of strings or two pairs of strings.", ), - "1.2345", - None, + "1.23", + "Options with wrong values are just discarded.", ), ## SignPadding ( @@ -394,6 +394,13 @@ def test_makeboxes_form(expr, form, head, subhead): "2 \u2062 a 0\n\n0 0\n", "Issue #182", ), + ## + ( + "NumberForm[N[10^ 5 Pi], 15, DigitBlock -> {4, 2}, ExponentStep->x]", + ("Value of option ExponentStep -> x is not a positive integer.",), + "31,4159.26 53 58 97 9", + "Options with wrong values are discarded, but other properties are kept.", + ), ], ) def test_private_doctests_output(str_expr, msgs, str_expected, fail_msg): @@ -407,3 +414,87 @@ def test_private_doctests_output(str_expr, msgs, str_expected, fail_msg): failure_message=fail_msg, expected_messages=msgs, ) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "fail_msg", "msgs"), + [ + ( + 'StringForm["This is symbol ``.", A]', + '"This is symbol A."', + "empty placeholder", + None, + ), + ( + 'StringForm["This is symbol `1`.", A]', + '"This is symbol A."', + "numerated placeholder", + None, + ), + ( + 'StringForm["This is symbol `0`.", A]', + 'StringForm["This is symbol `0`.", A]', + "placeholder index must be positive", + ( + 'Item 0 requested in "This is symbol `0`." out of range; 1 items available.', + ), + ), + ( + 'StringForm["This is symbol `symbol`.", A]', + 'StringForm["This is symbol `symbol`.", A]', + "placeholder must be an integer", + ( + 'Item 0 requested in "This is symbol `symbol`." out of range; 1 items available.', + ), + ), + ( + 'StringForm["This is symbol `5`.", A]', + 'StringForm["This is symbol `5`.", A]', + "placeholder index too large", + ( + 'Item 5 requested in "This is symbol `5`." out of range; 1 items available.', + ), + ), + ( + 'StringForm["This is symbol `2`, then `1`.", A, B]', + '"This is symbol B, then A."', + "numerated placeholder", + None, + ), + ( + 'StringForm["This is symbol `1`, then ``.", A, B]', + '"This is symbol A, then B."', + "empty placeholder use the next avaliable entry.", + None, + ), + ( + 'StringForm["This is symbol `2`, then ``.", A, B]', + 'StringForm["This is symbol `2`, then ``.", A, B]', + "no more available entry.", + ( + 'Item 3 requested in "This is symbol `2`, then ``." out of range; 2 items available.', + ), + ), + ( + 'StringForm["This is symbol `.", A]', + 'StringForm["This is symbol `.", A]', + "Unbalanced", + ("Unmatched backquote in This is symbol `..",), + ), + ( + r'StringForm["This is symbol \`.", A]', + '"This is symbol `."', + "literal backquote", + None, + ), + ], +) +def test_stringform(str_expr, str_expected, fail_msg, msgs): + """ """ + check_evaluation( + str_expr, + str_expected, + to_string_expr=True, + failure_message=fail_msg, + expected_messages=msgs, + ) diff --git a/test/builtin/test_number_form.py b/test/builtin/test_number_form.py index 9da4aa423..e49b3219b 100644 --- a/test/builtin/test_number_form.py +++ b/test/builtin/test_number_form.py @@ -26,7 +26,15 @@ def test_int_to_tuple_info( @pytest.mark.parametrize( - ("real", "digits", "expected", "exponent", "is_nonnegative"), + ( + "real", + "digits", + "expected", + "exponent", + "is_nonnegative", + "red_digits", + "precision", + ), [ # Using older uncorrected version of Real() # ( @@ -34,16 +42,28 @@ def test_int_to_tuple_info( # if Version(sympy.__version__) < Version("1.13.0") # else (Real(sympy.Float(0.0, 10)), 10, "0000000000", -1, True) # ), - (Real(sympy.Float(0.0, 10)), 10, "0", -10, True), - (Real(0), 1, "0", 0, True), - (Real(0), 2, "0", 0, True), - (Real(0.1), 2, "1", -1, True), - (Real(0.12), 2, "12", -1, True), - (Real(-0.12), 2, "12", -1, False), - (Real(3.141593), 10, "3141593", 0, True), + (Real(sympy.Float(0.0, 10)), 10, "0", -10, True, 10, 10), + (Real(0), 1, "0", 0, True, 1, 15), + (Real(0), 2, "0", 0, True, 2, 15), + (Real(0.1), 2, "1", -1, True, 2, 15), + (Real(0.12), 2, "12", -1, True, 2, 15), + (Real(-0.12), 2, "12", -1, False, 2, 15), + (Real(3.141593), 10, "3141593", 0, True, 10, 15), ], ) def test_real_to_tuple_info( - real: Real, digits: int, expected: str, exponent: int, is_nonnegative: bool + real: Real, + digits: int, + expected: str, + exponent: int, + is_nonnegative: bool, + red_digits: int, + precision: int, ): - assert real_to_tuple_info(real, digits) == (expected, exponent, is_nonnegative) + assert real_to_tuple_info(real, digits) == ( + expected, + exponent, + is_nonnegative, + red_digits, + precision, + )