Skip to content

Commit d2742f4

Browse files
committed
Add slice operator support in javascript
1 parent ee83de2 commit d2742f4

File tree

5 files changed

+345
-0
lines changed

5 files changed

+345
-0
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
//@ts-check
2+
import { describe, expect, test, beforeAll, afterAll } from "@jest/globals";
3+
import { WOQLClient, WOQL } from "../index.js";
4+
import { DbDetails } from "../dist/typescript/lib/typedef.js";
5+
import { Vars } from "../lib/woql.js";
6+
7+
let client: WOQLClient;
8+
const db01 = "db__test_woql_slice";
9+
10+
beforeAll(() => {
11+
client = new WOQLClient("http://127.0.0.1:6363", {
12+
user: "admin",
13+
organization: "admin",
14+
key: process.env.TDB_ADMIN_PASS ?? "root"
15+
});
16+
client.db(db01);
17+
});
18+
19+
describe("Integration tests for WOQL slice operator", () => {
20+
test("Create a database", async () => {
21+
const dbObj: DbDetails = {
22+
label: db01,
23+
comment: "test slice operator",
24+
schema: true
25+
};
26+
const result = await client.createDatabase(db01, dbObj);
27+
expect(result["@type"]).toEqual("api:DbCreateResponse");
28+
expect(result["api:status"]).toEqual("api:success");
29+
});
30+
31+
test("Basic slice: slice([A, B, C, D], 0, 2) returns [A, B]", async () => {
32+
let v = Vars("Result");
33+
const query = WOQL.slice(["A", "B", "C", "D"], v.Result, 0, 2);
34+
35+
const result = await client.query(query);
36+
expect(result?.bindings).toHaveLength(1);
37+
expect(result?.bindings[0].Result).toEqual([
38+
{ "@type": "xsd:string", "@value": "A" },
39+
{ "@type": "xsd:string", "@value": "B" }
40+
]);
41+
});
42+
43+
test("Negative indices: slice([A, B, C, D], -2) returns [C, D]", async () => {
44+
let v = Vars("Result");
45+
// Without end parameter - slice from -2 to end
46+
const query = WOQL.slice(["A", "B", "C", "D"], v.Result, -2);
47+
48+
const result = await client.query(query);
49+
expect(result?.bindings).toHaveLength(1);
50+
expect(result?.bindings[0].Result).toEqual([
51+
{ "@type": "xsd:string", "@value": "C" },
52+
{ "@type": "xsd:string", "@value": "D" }
53+
]);
54+
});
55+
56+
test("Out-of-bounds clamped: slice([A, B, C], 1, 100) returns [B, C]", async () => {
57+
let v = Vars("Result");
58+
const query = WOQL.slice(["A", "B", "C"], v.Result, 1, 100);
59+
60+
const result = await client.query(query);
61+
expect(result?.bindings).toHaveLength(1);
62+
expect(result?.bindings[0].Result).toEqual([
63+
{ "@type": "xsd:string", "@value": "B" },
64+
{ "@type": "xsd:string", "@value": "C" }
65+
]);
66+
});
67+
68+
test("Empty slice: slice([A, B, C], 2, 2) returns []", async () => {
69+
let v = Vars("Result");
70+
const query = WOQL.slice(["A", "B", "C"], v.Result, 2, 2);
71+
72+
const result = await client.query(query);
73+
expect(result?.bindings).toHaveLength(1);
74+
expect(result?.bindings[0].Result).toEqual([]);
75+
});
76+
77+
test("Slice with numeric list: slice([10, 20, 30, 40], 1, 3) returns [20, 30]", async () => {
78+
let v = Vars("Result");
79+
const query = WOQL.slice([10, 20, 30, 40], v.Result, 1, 3);
80+
81+
const result = await client.query(query);
82+
expect(result?.bindings).toHaveLength(1);
83+
expect(result?.bindings[0].Result).toEqual([
84+
{ "@type": "xsd:decimal", "@value": 20 },
85+
{ "@type": "xsd:decimal", "@value": 30 }
86+
]);
87+
});
88+
89+
test("Full range: slice([A, B, C, D], 0, 4) returns all elements", async () => {
90+
let v = Vars("Result");
91+
const query = WOQL.slice(["A", "B", "C", "D"], v.Result, 0, 4);
92+
93+
const result = await client.query(query);
94+
expect(result?.bindings).toHaveLength(1);
95+
expect(result?.bindings[0].Result).toEqual([
96+
{ "@type": "xsd:string", "@value": "A" },
97+
{ "@type": "xsd:string", "@value": "B" },
98+
{ "@type": "xsd:string", "@value": "C" },
99+
{ "@type": "xsd:string", "@value": "D" }
100+
]);
101+
});
102+
103+
test("Delete a database", async () => {
104+
const result = await client.deleteDatabase(db01);
105+
expect(result).toStrictEqual({
106+
"@type": "api:DbDeleteResponse",
107+
"api:status": "api:success"
108+
});
109+
});
110+
});

lib/query/woqlQuery.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,40 @@ WOQLQuery.prototype.length = function (inputVarList, resultVarName) {
14081408
return this;
14091409
};
14101410

1411+
/**
1412+
* Extracts a contiguous subsequence from a list, following JavaScript's slice() semantics
1413+
* @param {string|array|Var} inputList - Either a variable representing a list or a list of
1414+
* variables or literals
1415+
* @param {string|Var} resultVarName - A variable in which the sliced list is stored
1416+
* @param {number|string|Var} start - The start index (0-based, supports negative indices)
1417+
* @param {number|string|Var} [end] - The end index (exclusive, optional - defaults to list length)
1418+
* @returns {WOQLQuery} A WOQLQuery which contains the Slice pattern matching expression
1419+
* @example
1420+
* let [result] = vars("result")
1421+
* slice(["a", "b", "c", "d"], result, 1, 3) // result = ["b", "c"]
1422+
* slice(["a", "b", "c", "d"], result, -2) // result = ["c", "d"]
1423+
*/
1424+
WOQLQuery.prototype.slice = function (inputList, resultVarName, start, end) {
1425+
if (this.cursor['@type']) this.wrapCursorWithAnd();
1426+
this.cursor['@type'] = 'Slice';
1427+
this.cursor.list = this.cleanDataValue(inputList);
1428+
this.cursor.result = this.cleanDataValue(resultVarName);
1429+
if (typeof start === 'number') {
1430+
this.cursor.start = this.cleanObject(start, 'xsd:integer');
1431+
} else {
1432+
this.cursor.start = this.cleanDataValue(start);
1433+
}
1434+
// end is optional - only set if provided
1435+
if (end !== undefined) {
1436+
if (typeof end === 'number') {
1437+
this.cursor.end = this.cleanObject(end, 'xsd:integer');
1438+
} else {
1439+
this.cursor.end = this.cleanDataValue(end);
1440+
}
1441+
}
1442+
return this;
1443+
};
1444+
14111445
/**
14121446
*
14131447
* Logical negation of the contained subquery - if the subquery matches, the query

lib/woql.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,23 @@ WOQL.sum = function (subquery, total) {
875875
return new WOQLQuery().sum(subquery, total);
876876
};
877877

878+
/**
879+
* Extracts a contiguous subsequence from a list, following JavaScript's slice() semantics
880+
* @param {string|array|Var} inputList - Either a variable representing a list or a list of
881+
* variables or literals
882+
* @param {string|Var} resultVarName - A variable in which the sliced list is stored
883+
* @param {number|string|Var} start - The start index (0-based, supports negative indices)
884+
* @param {number|string|Var} [end] - The end index (exclusive, optional - defaults to list length)
885+
* @returns {WOQLQuery} A WOQLQuery which contains the Slice pattern matching expression
886+
* @example
887+
* let [result] = vars("result")
888+
* slice(["a", "b", "c", "d"], result, 1, 3) // result = ["b", "c"]
889+
* slice(["a", "b", "c", "d"], result, -2) // result = ["c", "d"]
890+
*/
891+
WOQL.slice = function (inputList, resultVarName, start, end) {
892+
return new WOQLQuery().slice(inputList, resultVarName, start, end);
893+
};
894+
878895
/**
879896
*
880897
* Specifies an offset position in the results to start listing results from

test/woqlJson/woqlSliceJson.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* Expected JSON output for WOQL slice operator tests
3+
*/
4+
5+
const WOQL_SLICE_JSON = {
6+
// Basic slice with all parameters
7+
basicSlice: {
8+
'@type': 'Slice',
9+
list: {
10+
'@type': 'DataValue',
11+
list: [
12+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'a' } },
13+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'b' } },
14+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'c' } },
15+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'd' } },
16+
],
17+
},
18+
result: { '@type': 'DataValue', variable: 'Result' },
19+
start: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': 1 } },
20+
end: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': 3 } },
21+
},
22+
23+
// Slice with negative indices
24+
negativeIndices: {
25+
'@type': 'Slice',
26+
list: {
27+
'@type': 'DataValue',
28+
list: [
29+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'a' } },
30+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'b' } },
31+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'c' } },
32+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'd' } },
33+
],
34+
},
35+
result: { '@type': 'DataValue', variable: 'Result' },
36+
start: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': -2 } },
37+
end: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': -1 } },
38+
},
39+
40+
// Slice without end parameter (optional)
41+
withoutEnd: {
42+
'@type': 'Slice',
43+
list: {
44+
'@type': 'DataValue',
45+
list: [
46+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'a' } },
47+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'b' } },
48+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'c' } },
49+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'd' } },
50+
],
51+
},
52+
result: { '@type': 'DataValue', variable: 'Result' },
53+
start: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': 1 } },
54+
// Note: no 'end' property when end is omitted
55+
},
56+
57+
// Slice with variable as list input
58+
variableList: {
59+
'@type': 'Slice',
60+
list: { '@type': 'DataValue', variable: 'MyList' },
61+
result: { '@type': 'DataValue', variable: 'Result' },
62+
start: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': 0 } },
63+
end: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': 2 } },
64+
},
65+
66+
// Slice with variable indices
67+
variableIndices: {
68+
'@type': 'Slice',
69+
list: {
70+
'@type': 'DataValue',
71+
list: [
72+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'x' } },
73+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'y' } },
74+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'z' } },
75+
],
76+
},
77+
result: { '@type': 'DataValue', variable: 'Result' },
78+
start: { '@type': 'DataValue', variable: 'Start' },
79+
end: { '@type': 'DataValue', variable: 'End' },
80+
},
81+
82+
// Slice from start (index 0)
83+
fromStart: {
84+
'@type': 'Slice',
85+
list: {
86+
'@type': 'DataValue',
87+
list: [
88+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'a' } },
89+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'b' } },
90+
{ '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'c' } },
91+
],
92+
},
93+
result: { '@type': 'DataValue', variable: 'Result' },
94+
start: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': 0 } },
95+
end: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': 2 } },
96+
},
97+
};
98+
99+
module.exports = WOQL_SLICE_JSON;

test/woqlSlice.spec.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Unit tests for WOQL slice operator
3+
*
4+
* Tests the JavaScript client binding for slice(list, result, start, end?)
5+
*/
6+
7+
const { expect } = require('chai');
8+
const WOQL = require('../lib/woql');
9+
const WOQL_SLICE_JSON = require('./woqlJson/woqlSliceJson');
10+
11+
describe('WOQL slice operator', () => {
12+
describe('AC-1: Basic slicing', () => {
13+
it('generates correct JSON for slice with start and end', () => {
14+
const woqlObject = WOQL.slice(['a', 'b', 'c', 'd'], 'v:Result', 1, 3);
15+
expect(woqlObject.json()).to.eql(WOQL_SLICE_JSON.basicSlice);
16+
});
17+
});
18+
19+
describe('AC-3: Negative indices', () => {
20+
it('generates correct JSON for slice with negative indices', () => {
21+
const woqlObject = WOQL.slice(['a', 'b', 'c', 'd'], 'v:Result', -2, -1);
22+
expect(woqlObject.json()).to.eql(WOQL_SLICE_JSON.negativeIndices);
23+
});
24+
});
25+
26+
describe('Optional end parameter', () => {
27+
it('generates correct JSON when end is omitted', () => {
28+
const woqlObject = WOQL.slice(['a', 'b', 'c', 'd'], 'v:Result', 1);
29+
expect(woqlObject.json()).to.eql(WOQL_SLICE_JSON.withoutEnd);
30+
});
31+
});
32+
33+
describe('Variable inputs', () => {
34+
it('generates correct JSON with variable as list input', () => {
35+
const woqlObject = WOQL.slice('v:MyList', 'v:Result', 0, 2);
36+
expect(woqlObject.json()).to.eql(WOQL_SLICE_JSON.variableList);
37+
});
38+
39+
it('generates correct JSON with variable indices', () => {
40+
const woqlObject = WOQL.slice(['x', 'y', 'z'], 'v:Result', 'v:Start', 'v:End');
41+
expect(woqlObject.json()).to.eql(WOQL_SLICE_JSON.variableIndices);
42+
});
43+
});
44+
45+
describe('AC-7: Full range', () => {
46+
it('generates correct JSON for slice from start', () => {
47+
const woqlObject = WOQL.slice(['a', 'b', 'c'], 'v:Result', 0, 2);
48+
expect(woqlObject.json()).to.eql(WOQL_SLICE_JSON.fromStart);
49+
});
50+
});
51+
52+
describe('Method chaining', () => {
53+
it('works with method chaining via WOQLQuery instance', () => {
54+
const woqlObject = WOQL.slice(['a', 'b', 'c'], 'v:Result', 0, 2);
55+
expect(woqlObject.json()['@type']).to.equal('Slice');
56+
});
57+
58+
it('chains with and() correctly', () => {
59+
const woqlObject = WOQL.eq('v:MyList', ['x', 'y', 'z'])
60+
.and()
61+
.slice('v:MyList', 'v:Result', 1, 3);
62+
63+
const json = woqlObject.json();
64+
expect(json['@type']).to.equal('And');
65+
expect(json.and).to.be.an('array').that.has.lengthOf(2);
66+
expect(json.and[1]['@type']).to.equal('Slice');
67+
});
68+
});
69+
70+
describe('Edge cases', () => {
71+
it('handles empty list', () => {
72+
const woqlObject = WOQL.slice([], 'v:Result', 0, 1);
73+
const json = woqlObject.json();
74+
expect(json['@type']).to.equal('Slice');
75+
expect(json.list.list).to.be.an('array').that.has.lengthOf(0);
76+
});
77+
78+
it('handles single element slice', () => {
79+
const woqlObject = WOQL.slice(['only'], 'v:Result', 0, 1);
80+
const json = woqlObject.json();
81+
expect(json['@type']).to.equal('Slice');
82+
expect(json.list.list).to.be.an('array').that.has.lengthOf(1);
83+
});
84+
});
85+
});

0 commit comments

Comments
 (0)