diff --git a/mathics/builtin/list/eol.py b/mathics/builtin/list/eol.py index 21eb8072c..259f89254 100644 --- a/mathics/builtin/list/eol.py +++ b/mathics/builtin/list/eol.py @@ -21,6 +21,7 @@ from mathics.core.builtin import BinaryOperator, Builtin from mathics.core.convert.expression import to_mathics_list from mathics.core.convert.python import from_python +from mathics.core.evaluation import Evaluation from mathics.core.exceptions import ( InvalidLevelspecError, MessageException, @@ -580,39 +581,74 @@ class First(Builtin):
'First[$expr$]'
returns the first element in $expr$. + +
'First[$expr$, $def$]' +
returns the first element in $expr$ if it exists or $def$ otherwise.
'First[$expr$]' is equivalent to '$expr$[[1]]'. >> First[{a, b, c}] = a + + The first argument need not be a list: >> First[a + b + c] = a + + However, the first argument must be Nonatomic when there is a single argument: >> First[x] - : Nonatomic expression expected. + : Nonatomic expression expected at position 1 in First[x]. = First[x] + + Or if it is not, but a second default argument is provided, that is \ + evaluated and returned: + + >> First[10, 1+2] + = 3 + >> First[{}] : {} has zero length and no first element. = First[{}] + + As before, the first argument is empty, but a default argument is given, \ + evaluate and return the second argument: + >> First[{}, 1+2] + = 3 """ + attributes = A_HOLD_REST | A_PROTECTED messages = { - "normal": "Nonatomic expression expected.", + "argt": "First called with `1` arguments; 1 or 2 arguments are expected.", + "normal": "Nonatomic expression expected at position 1 in `1`.", "nofirst": "`1` has zero length and no first element.", } summary_text = "first element of a list or expression" - def eval(self, expr, evaluation): - "First[expr_]" + # FIXME: the code and the code for Last are similar and can be DRY'd + def eval(self, expr, evaluation: Evaluation, expression: Expression): + "First[expr__]" if isinstance(expr, Atom): - evaluation.message("First", "normal") + evaluation.message("First", "normal", expression) return - if len(expr.elements) == 0: + expr_len = len(expr.elements) + if expr_len == 0: evaluation.message("First", "nofirst", expr) return + if expr_len > 2 and expr.head is SymbolSequence: + evaluation.message("First", "argt", expr_len) + return + + first_elem = expr.elements[0] + + if expr.head == SymbolSequence or ( + not isinstance(expr, ListExpression) + and len == 2 + and isinstance(first_elem, Atom) + ): + return expr.elements[1] - return expr.elements[0] + return first_elem class FirstCase(Builtin): @@ -813,35 +849,64 @@ class Last(Builtin):
'Last[$expr$]'
returns the last element in $expr$. + +
'Last[$expr$, $def$]' +
returns the last element in $expr$ if it exists or $def$ otherwise.
'Last[$expr$]' is equivalent to '$expr$[[-1]]'. >> Last[{a, b, c}] = c - >> Last[x] - : Nonatomic expression expected. - = Last[x] + + The first argument need not be a list: + >> Last[a + b + c] + = c + + However, the first argument must be Nonatomic when there is a single argument: + >> Last[10] + : Nonatomic expression expected at position 1 in Last[10]. + = Last[10] + + Or if it is not, but a second default argument is provided, that is \ + evaluated and returned: + + >> Last[10, 1+2] + = 3 + + >> Last[{}] : {} has zero length and no last element. = Last[{}] + + As before, the first argument is empty, but since default argument is given, \ + evaluate and return the second argument: + >> Last[{}, 1+2] + = 3 """ + attributes = A_HOLD_REST | A_PROTECTED messages = { - "normal": "Nonatomic expression expected.", + "argt": "Last called with `1` arguments; 1 or 2 arguments are expected.", + "normal": "Nonatomic expression expected at position 1 in `1`.", "nolast": "`1` has zero length and no last element.", } summary_text = "last element of a list or expression" - def eval(self, expr, evaluation): - "Last[expr_]" + # FIXME: the code and the code for First are similar and can be DRY'd + def eval(self, expr, evaluation: Evaluation, expression: Expression): + "Last[expr__]" if isinstance(expr, Atom): - evaluation.message("Last", "normal") + evaluation.message("Last", "normal", expression) return - if len(expr.elements) == 0: + expr_len = len(expr.elements) + if expr_len == 0: evaluation.message("Last", "nolast", expr) return + if expr_len > 2 and expr.head is SymbolSequence: + evaluation.message("Last", "argt", expr_len) + return return expr.elements[-1] @@ -906,17 +971,21 @@ class Most(Builtin): >> Most[a + b + c] = a + b >> Most[x] - : Nonatomic expression expected. + : Nonatomic expression expected at position 1 in Most[x]. = Most[x] """ + messages = { + "normal": "Nonatomic expression expected at position 1 in `1`.", + } + summary_text = "remove the last element" - def eval(self, expr, evaluation): + def eval(self, expr, evaluation: Evaluation, expression: Expression): "Most[expr_]" if isinstance(expr, Atom): - evaluation.message("Most", "normal") + evaluation.message("Most", "normal", expression) return return expr.slice(expr.head, slice(0, -1), evaluation) @@ -1395,7 +1464,7 @@ class Rest(Builtin): >> Rest[a + b + c] = b + c >> Rest[x] - : Nonatomic expression expected. + : Nonatomic expression expected at position 1 in Rest[x]. = Rest[x] >> Rest[{}] : Cannot take Rest of expression {} with length zero. @@ -1403,16 +1472,16 @@ class Rest(Builtin): """ messages = { - "normal": "Nonatomic expression expected.", + "normal": "Nonatomic expression expected at position 1 in `1`.", "norest": "Cannot take Rest of expression `1` with length zero.", } summary_text = "remove the first element" - def eval(self, expr, evaluation): + def eval(self, expr, evaluation: Evaluation, expression: Expression): "Rest[expr_]" if isinstance(expr, Atom): - evaluation.message("Rest", "normal") + evaluation.message("Rest", "normal", expression) return if len(expr.elements) == 0: evaluation.message("Rest", "norest", expr) diff --git a/mathics/eval/files_io/files.py b/mathics/eval/files_io/files.py index 3678f6de1..a47b0c406 100644 --- a/mathics/eval/files_io/files.py +++ b/mathics/eval/files_io/files.py @@ -73,7 +73,7 @@ def eval_Get(path: str, evaluation: Evaluation, trace_fn: Optional[Callable]): continue result = query.evaluate(evaluation) except IOError: - evaluation.message("General", "noopen", path) + evaluation.message("Get", "noopen", path) return SymbolFailed except MessageException as e: e.message(evaluation) diff --git a/test/builtin/list/__init__.py b/test/builtin/list/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/builtin/list/test_eol.py b/test/builtin/list/test_eol.py index a0f223998..79a4f0bab 100644 --- a/test/builtin/list/test_eol.py +++ b/test/builtin/list/test_eol.py @@ -82,6 +82,12 @@ "Drop[{1, 2, 3, 4, 5, 6}, {-5, -2, -2}]", None, ), + ( + "First[a, b, c]", + ("First called with 3 arguments; 1 or 2 arguments are expected.",), + "First[a, b, c]", + None, + ), ('FirstPosition[{1, 2, 3}, _?StringQ, "NoStrings"]', None, "NoStrings", None), ("FirstPosition[a, a]", None, "{}", None), ( @@ -155,7 +161,13 @@ ("a = {2,3,4}; i = 1; a[[i]] = 0; a", None, "{0, 3, 4}", None), ## Negative step ("{1,2,3,4,5}[[3;;1;;-1]]", None, "{3, 2, 1}", None), - ("{1, 2, 3, 4, 5}[[;; ;; -1]]", None, "{5, 4, 3, 2, 1}", "MMA bug"), + ("ClearAll[a]", None, "Null", None), + ( + "Last[a, b, c]", + ("Last called with 3 arguments; 1 or 2 arguments are expected.",), + "Last[a, b, c]", + None, + ), ("Range[11][[-3 ;; 2 ;; -2]]", None, "{9, 7, 5, 3}", None), ("Range[11][[-3 ;; -7 ;; -3]]", None, "{9, 6}", None), ("Range[11][[7 ;; -7;; -2]]", None, "{7, 5}", None),