diff --git a/mathics/builtin/list/associations.py b/mathics/builtin/list/associations.py index c378bc9c3..393f09c87 100644 --- a/mathics/builtin/list/associations.py +++ b/mathics/builtin/list/associations.py @@ -8,16 +8,16 @@ actual keys found in the collection. """ - from mathics.builtin.box.layout import RowBox from mathics.core.atoms import Integer -from mathics.core.attributes import A_HOLD_ALL_COMPLETE, A_PROTECTED +from mathics.core.attributes import A_HOLD_ALL_COMPLETE, A_PROTECTED, A_READ_PROTECTED from mathics.core.builtin import Builtin, Test from mathics.core.convert.expression import to_mathics_list from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.symbols import Symbol, SymbolTrue from mathics.core.systemsymbols import SymbolAssociation, SymbolMakeBoxes, SymbolMissing +from mathics.eval.list.associations import eval_Lookup, eval_Lookup_multiple_keys from mathics.eval.lists import list_boxes @@ -249,18 +249,60 @@ class Lookup(Builtin):
Lookup[$assoc$, $key$]
looks up the value associated with $key$ in the association $assoc$, \ - or Missing[$KeyAbsent$]. + returning Missing[$KeyAbsent$, $key$] if the key is not found. +
Lookup[$assoc$, $key$, $default$] +
looks up the value associated with $key$ in the association $assoc$, \ + returning $default$ if the key is not found. +
Lookup[$assoc$, {$key_1$, $key_2$, ...}] +
looks up multiple keys and returns a list of values.
+ + Look up the value associagted with key a: + >> Lookup[<|a -> 1, b -> 2|>, a] + = 1 + + When a key is not found, a Missing object is returned by default: + >> Lookup[<|a -> 1, b -> 2|>, c] + = Missing[KeyAbsent, c] + + Provide a default value to be used when the key is not found: + >> Lookup[<|a -> 1, b -> 2|>, c, -1] + = -1 + + Use the operator form of Lookup: + >> Lookup[<|a -> 1, b -> 2|>, {a, b}] + = {1, 2} + + Look up multiple keys at once: + >> Lookup[<|a -> 1, b -> 2|>, {a, b, c}] + = {1, 2, Missing[KeyAbsent, c]} + """ - attributes = A_HOLD_ALL_COMPLETE - rules = { - "Lookup[assoc_?AssociationQ, key_, default_]": "FirstCase[assoc, _[Verbatim[key], val_] :> val, default]", - "Lookup[assoc_?AssociationQ, key_]": 'Lookup[assoc, key, Missing["KeyAbsent", key]]', + attributes = A_PROTECTED | A_READ_PROTECTED + + messages = { + "invrl": "The argument `1` is not a valid Association or a list of rules.", } summary_text = "perform lookup of a value by key, returning a specified default if it is not found" + def eval_assoc_key(self, assoc, key, evaluation: Evaluation): + """Lookup[assoc_Association, key_]""" + return eval_Lookup(assoc, key, None, evaluation) + + def eval_assoc_key_default(self, assoc, key, default, evaluation: Evaluation): + """Lookup[assoc_Association, key_, default_]""" + return eval_Lookup(assoc, key, default, evaluation) + + def eval_assoc_keys(self, assoc, keys, evaluation: Evaluation): + """Lookup[assoc_Association, keys_List]""" + return eval_Lookup_multiple_keys(assoc, keys, None, evaluation) + + def eval_assoc_keys_default(self, assoc, keys, default, evaluation: Evaluation): + """Lookup[assoc_Association, keys_List, default_]""" + return eval_Lookup_multiple_keys(assoc, keys, default, evaluation) + class Missing(Builtin): """ diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index eea8861ee..7b97ede03 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -184,6 +184,7 @@ SymbolIntegrate = Symbol("System`Integrate") SymbolInterpretationBox = Symbol("System`InterpretationBox") SymbolKey = Symbol("System`Key") +SymbolKeyAbsent = Symbol("System`KeyAbsent") SymbolKhinchin = Symbol("System`Khinchin") SymbolLabelStyle = Symbol("System`LabelStyle") SymbolLast = Symbol("System`Last") diff --git a/mathics/eval/list/__init__.py b/mathics/eval/list/__init__.py index e69de29bb..f9315483b 100644 --- a/mathics/eval/list/__init__.py +++ b/mathics/eval/list/__init__.py @@ -0,0 +1,4 @@ +""" +Evaluation routines and associated code for Built-in functions found under module +mathics.builtins.list. +""" diff --git a/mathics/eval/list/associations.py b/mathics/eval/list/associations.py new file mode 100644 index 000000000..edb2f0a1f --- /dev/null +++ b/mathics/eval/list/associations.py @@ -0,0 +1,50 @@ +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.systemsymbols import SymbolKeyAbsent, SymbolMissing + + +def eval_Lookup(assoc, key, default, evaluation: Evaluation): + """Evaluation method for Lookup.""" + + if assoc.has_form("Association", None): + # Search through association elements (rules) + for element in assoc.elements: + if element.has_form(("Rule", "RuleDelayed"), 2): + if element.elements[0] == key: + return element.elements[1] + + # Key not found + if default is not None: + return default + else: + return Expression(SymbolMissing, SymbolKeyAbsent, key) + + elif isinstance(assoc, ListExpression): + # Search through list of rules + for element in assoc.elements: + if element.has_form(("Rule", "RuleDelayed"), 2): + if element.elements[0] == key: + return element.elements[1] + + # Key not found + if default is not None: + return default + else: + return Expression(SymbolMissing, SymbolKeyAbsent, key) + + elif assoc.has_form(("Rule", "RuleDelayed"), 2): + if assoc.elements[0] == key: + return assoc.elements[1] + return None + + else: + evaluation.message("Lookup", "invrl", assoc) + # Should we return SymbolFailed? + return None + + +def eval_Lookup_multiple_keys(assoc, keys, default, evaluation: Evaluation): + """Evaluation method for Lookup with multiple keys, threading over the key list.""" + results = [eval_Lookup(assoc, key, default, evaluation) for key in keys.elements] + return ListExpression(*results) diff --git a/test/builtin/list/test_association.py b/test/builtin/list/test_association.py index 73403f33b..7f1587f0d 100644 --- a/test/builtin/list/test_association.py +++ b/test/builtin/list/test_association.py @@ -263,3 +263,24 @@ def test_map_over_associations( failure_message=assert_message, expected_messages=expected_messages, ) + + +@pytest.mark.parametrize( + ("str_expr", "expected_messages", "str_expected", "assert_message"), + [ + ( + 'a=Association[{"F":>1,"G":>2}]; Lookup[a, "H"]', + None, + "Missing[KeyAbsent, H]", + "Lookup test on an association variable where the key is not found.", + ), + ("ClearAll[a];", None, "Null", None), + ], +) +def test_lookup(str_expr, expected_messages, str_expected, assert_message): + check_evaluation( + str_expr, + str_expected, + failure_message=assert_message, + expected_messages=expected_messages, + ) diff --git a/test/builtin/list/test_eol.py b/test/builtin/list/test_eol.py index 7941903f9..b4fd29317 100644 --- a/test/builtin/list/test_eol.py +++ b/test/builtin/list/test_eol.py @@ -10,6 +10,7 @@ @pytest.mark.parametrize( ("str_expr", "expected_messages", "str_expected", "assert_message"), [ + ("ClearAll[a];", None, "Null", None), ( "Append[a, b]", ("Nonatomic expression expected at position 1 in Append[a, b].",),