Skip to content

Commit eee1db7

Browse files
authored
Merge pull request #453 from hoijnet/issue/2304-slice-operator
Add slice operator support in python
2 parents 9b4b9c1 + 653f94d commit eee1db7

File tree

3 files changed

+200
-0
lines changed

3 files changed

+200
-0
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""
2+
Unit tests for WOQL slice operator
3+
4+
Tests the Python client binding for slice(input_list, result, start, end=None)
5+
"""
6+
7+
from terminusdb_client.woqlquery.woql_query import WOQLQuery
8+
9+
from .woqljson.woqlSliceJson import WOQL_SLICE_JSON
10+
11+
12+
class TestWOQLSlice:
13+
"""Test cases for the slice operator"""
14+
15+
def test_basic_slice(self):
16+
"""Basic slicing - slice([a,b,c,d], result, 1, 3) returns [b,c]"""
17+
woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 1, 3)
18+
assert woql_object.to_dict() == WOQL_SLICE_JSON["basicSlice"]
19+
20+
def test_negative_indices(self):
21+
"""Negative indices - slice([a,b,c,d], result, -2, -1) returns [c]"""
22+
woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", -2, -1)
23+
assert woql_object.to_dict() == WOQL_SLICE_JSON["negativeIndices"]
24+
25+
def test_without_end(self):
26+
"""Optional end - slice([a,b,c,d], result, 1) returns [b,c,d]"""
27+
woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 1)
28+
assert woql_object.to_dict() == WOQL_SLICE_JSON["withoutEnd"]
29+
30+
def test_variable_list(self):
31+
"""Variable list input - slice(v:MyList, result, 0, 2)"""
32+
woql_object = WOQLQuery().slice("v:MyList", "v:Result", 0, 2)
33+
assert woql_object.to_dict() == WOQL_SLICE_JSON["variableList"]
34+
35+
def test_variable_indices(self):
36+
"""Variable indices - slice([x,y,z], result, v:Start, v:End)"""
37+
woql_object = WOQLQuery().slice(["x", "y", "z"], "v:Result", "v:Start", "v:End")
38+
assert woql_object.to_dict() == WOQL_SLICE_JSON["variableIndices"]
39+
40+
def test_empty_list(self):
41+
"""Empty list - slice([], result, 0, 1) returns []"""
42+
woql_object = WOQLQuery().slice([], "v:Result", 0, 1)
43+
assert woql_object.to_dict() == WOQL_SLICE_JSON["emptyList"]
44+
45+
def test_slice_type(self):
46+
"""Verify the @type is correctly set to 'Slice'"""
47+
woql_object = WOQLQuery().slice(["a", "b"], "v:Result", 0, 1)
48+
assert woql_object.to_dict()["@type"] == "Slice"
49+
50+
def test_chaining_with_and(self):
51+
"""Test that slice works with method chaining via woql_and"""
52+
woql_object = WOQLQuery().woql_and(
53+
WOQLQuery().eq("v:MyList", ["a", "b", "c"]),
54+
WOQLQuery().slice("v:MyList", "v:Result", 1, 3),
55+
)
56+
result = woql_object.to_dict()
57+
assert result["@type"] == "And"
58+
assert len(result["and"]) == 2
59+
assert result["and"][1]["@type"] == "Slice"
60+
61+
def test_single_element_slice(self):
62+
"""Single element - slice([a,b,c,d], result, 1, 2) returns [b]"""
63+
woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 1, 2)
64+
result = woql_object.to_dict()
65+
assert result["@type"] == "Slice"
66+
assert result["start"]["data"]["@value"] == 1
67+
assert result["end"]["data"]["@value"] == 2
68+
69+
def test_full_range(self):
70+
"""Full range - slice([a,b,c,d], result, 0, 4) returns [a,b,c,d]"""
71+
woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 0, 4)
72+
result = woql_object.to_dict()
73+
assert result["@type"] == "Slice"
74+
assert result["start"]["data"]["@value"] == 0
75+
assert result["end"]["data"]["@value"] == 4
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Expected JSON output for WOQL slice operator tests"""
2+
3+
WOQL_SLICE_JSON = {
4+
"basicSlice": {
5+
"@type": "Slice",
6+
"list": {
7+
"@type": "DataValue",
8+
"list": [
9+
{"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "a"}},
10+
{"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "b"}},
11+
{"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "c"}},
12+
{"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "d"}},
13+
],
14+
},
15+
"result": {"@type": "DataValue", "variable": "Result"},
16+
"start": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 1}},
17+
"end": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 3}},
18+
},
19+
"negativeIndices": {
20+
"@type": "Slice",
21+
"list": {
22+
"@type": "DataValue",
23+
"list": [
24+
{"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "a"}},
25+
{"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "b"}},
26+
{"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "c"}},
27+
{"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "d"}},
28+
],
29+
},
30+
"result": {"@type": "DataValue", "variable": "Result"},
31+
"start": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": -2}},
32+
"end": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": -1}},
33+
},
34+
"withoutEnd": {
35+
"@type": "Slice",
36+
"list": {
37+
"@type": "DataValue",
38+
"list": [
39+
{"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "a"}},
40+
{"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "b"}},
41+
{"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "c"}},
42+
{"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "d"}},
43+
],
44+
},
45+
"result": {"@type": "DataValue", "variable": "Result"},
46+
"start": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 1}},
47+
# Note: no 'end' property when end is omitted
48+
},
49+
"variableList": {
50+
"@type": "Slice",
51+
"list": {"@type": "DataValue", "variable": "MyList"},
52+
"result": {"@type": "DataValue", "variable": "Result"},
53+
"start": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 0}},
54+
"end": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 2}},
55+
},
56+
"variableIndices": {
57+
"@type": "Slice",
58+
"list": {
59+
"@type": "DataValue",
60+
"list": [
61+
{"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "x"}},
62+
{"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "y"}},
63+
{"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "z"}},
64+
],
65+
},
66+
"result": {"@type": "DataValue", "variable": "Result"},
67+
"start": {"@type": "DataValue", "variable": "Start"},
68+
"end": {"@type": "DataValue", "variable": "End"},
69+
},
70+
"emptyList": {
71+
"@type": "Slice",
72+
"list": {"@type": "DataValue", "list": []},
73+
"result": {"@type": "DataValue", "variable": "Result"},
74+
"start": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 0}},
75+
"end": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 1}},
76+
},
77+
}

terminusdb_client/woqlquery/woql_query.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2648,6 +2648,54 @@ def sum(self, user_input, output):
26482648
self._cursor["sum"] = self._clean_data_value(output)
26492649
return self
26502650

2651+
def slice(self, input_list, result, start, end=None):
2652+
"""
2653+
Extracts a contiguous subsequence from a list, following slice() semantics.
2654+
2655+
Parameters
2656+
----------
2657+
input_list : list or str
2658+
A list of values or a variable representing a list
2659+
result : str
2660+
A variable that stores the sliced result
2661+
start : int or str
2662+
The start index (0-based, supports negative indices)
2663+
end : int or str, optional
2664+
The end index (exclusive). If omitted, takes the rest of the list
2665+
2666+
Returns
2667+
-------
2668+
WOQLQuery object
2669+
query object that can be chained and/or execute
2670+
2671+
Examples
2672+
--------
2673+
>>> WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 1, 3) # ["b", "c"]
2674+
>>> WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", -2) # ["c", "d"]
2675+
"""
2676+
if input_list and input_list == "args":
2677+
return ["list", "result", "start", "end"]
2678+
if self._cursor.get("@type"):
2679+
self._wrap_cursor_with_and()
2680+
self._cursor["@type"] = "Slice"
2681+
self._cursor["list"] = self._data_list(input_list)
2682+
self._cursor["result"] = self._clean_data_value(result)
2683+
if isinstance(start, int):
2684+
self._cursor["start"] = self._clean_data_value(
2685+
{"@type": "xsd:integer", "@value": start}
2686+
)
2687+
else:
2688+
self._cursor["start"] = self._clean_data_value(start)
2689+
# end is optional
2690+
if end is not None:
2691+
if isinstance(end, int):
2692+
self._cursor["end"] = self._clean_data_value(
2693+
{"@type": "xsd:integer", "@value": end}
2694+
)
2695+
else:
2696+
self._cursor["end"] = self._clean_data_value(end)
2697+
return self
2698+
26512699
def start(self, start, query=None):
26522700
"""Specifies that the start of the query returned
26532701

0 commit comments

Comments
 (0)