Repository: iris-hep/qastle

Issue:
`Last` node type
Just the reverse of `First`.

Discussion:

Patch:
diff --git a/README.md b/README.md
index b57b82e..27155e5 100644
--- a/README.md
+++ b/README.md
@@ -69,6 +69,7 @@ All defined s-expressions are listed here, though this specification will be exp
   - SelectMany: `(SelectMany <source> <selector>)`
     - `selector` must be a `lambda` with one argument
   - First: `(First <source>)`
+  - Last: `(Last <source>)`
   - Aggregate: `(Aggregate <source> <seed> <func>)`
     - `func` must be a `lambda` with two arguments
   - Count: `(Count <source>)`
diff --git a/qastle/linq_util.py b/qastle/linq_util.py
index b123349..df6eb79 100644
--- a/qastle/linq_util.py
+++ b/qastle/linq_util.py
@@ -19,6 +19,10 @@ class First(ast.AST):
     _fields = ['source']
 
 
+class Last(ast.AST):
+    _fields = ['source']
+
+
 class Aggregate(ast.AST):
     _fields = ['source', 'seed', 'func']
 
@@ -55,6 +59,7 @@ linq_operator_names = ('Where',
                        'Select',
                        'SelectMany',
                        'First',
+                       'Last',
                        'Aggregate',
                        'Count',
                        'Max',
@@ -115,6 +120,10 @@ class InsertLINQNodesTransformer(ast.NodeTransformer):
             if len(args) != 0:
                 raise SyntaxError('First() call must have zero arguments')
             return First(source=self.visit(source))
+        elif function_name == 'Last':
+            if len(args) != 0:
+                raise SyntaxError('Last() call must have zero arguments')
+            return Last(source=self.visit(source))
         elif function_name == 'Aggregate':
             if len(args) != 2:
                 raise SyntaxError('Aggregate() call must have exactly two arguments; found'
diff --git a/qastle/transform.py b/qastle/transform.py
index 203adb6..e13b16d 100644
--- a/qastle/transform.py
+++ b/qastle/transform.py
@@ -1,4 +1,4 @@
-from .linq_util import (Where, Select, SelectMany, First, Aggregate, Count, Max,
+from .linq_util import (Where, Select, SelectMany, First, Last, Aggregate, Count, Max,
                         Min, Sum, Zip, OrderBy, Choose)
 from .ast_util import wrap_ast, unwrap_ast
 
@@ -183,6 +183,9 @@ class PythonASTToTextASTTransformer(ast.NodeVisitor):
     def visit_First(self, node):
         return self.make_composite_node_string('First', self.visit(node.source))
 
+    def visit_Last(self, node):
+        return self.make_composite_node_string('Last', self.visit(node.source))
+
     def visit_Aggregate(self, node):
         return self.make_composite_node_string('Aggregate',
                                                self.visit(node.source),
@@ -413,6 +416,11 @@ class TextASTToPythonASTTransformer(lark.Transformer):
                 raise SyntaxError('First node must have one field; found ' + str(len(fields)))
             return First(source=fields[0])
 
+        elif node_type == 'Last':
+            if len(fields) != 1:
+                raise SyntaxError('Last node must have one field; found ' + str(len(fields)))
+            return Last(source=fields[0])
+
         elif node_type == 'Aggregate':
             if len(fields) != 3:
                 raise SyntaxError('Aggregate node must have three fields; found '


Test patch:
diff --git a/tests/test_ast_language.py b/tests/test_ast_language.py
index 1383477..b83cb8d 100644
--- a/tests/test_ast_language.py
+++ b/tests/test_ast_language.py
@@ -231,3 +231,9 @@ def test_Choose():
                          n=unwrap_ast(ast.parse('2')))
     assert_equivalent_python_ast_and_text_ast(wrap_ast(choose_node),
                                               '(Choose data_source 2)')
+
+
+def test_Last():
+    first_node = Last(source=unwrap_ast(ast.parse('data_source')))
+    assert_equivalent_python_ast_and_text_ast(wrap_ast(first_node),
+                                              '(Last data_source)')
diff --git a/tests/test_linq_util.py b/tests/test_linq_util.py
index c603f3d..cfc7e9c 100644
--- a/tests/test_linq_util.py
+++ b/tests/test_linq_util.py
@@ -316,3 +316,22 @@ def test_choose_composite():
 def test_choose_bad():
     with pytest.raises(SyntaxError):
         insert_linq_nodes(ast.parse('the_source.Choose()'))
+
+
+def test_last():
+    initial_ast = ast.parse("the_source.Last()")
+    final_ast = insert_linq_nodes(initial_ast)
+    expected_ast = wrap_ast(Last(source=unwrap_ast(ast.parse('the_source'))))
+    assert_ast_nodes_are_equal(final_ast, expected_ast)
+
+
+def test_last_composite():
+    initial_ast = ast.parse("the_source.Last().Last()")
+    final_ast = insert_linq_nodes(initial_ast)
+    expected_ast = wrap_ast(Last(source=Last(source=unwrap_ast(ast.parse('the_source')))))
+    assert_ast_nodes_are_equal(final_ast, expected_ast)
+
+
+def test_last_bad():
+    with pytest.raises(SyntaxError):
+        insert_linq_nodes(ast.parse('the_source.Last(None)'))