jsqry is a simple JS lib to query objects/arrays.
API of jsqry is truly minimalistic and consists only of two functions:
query and first.
jsqry.query(target, query[, ...args])
Query target list or object by query. Arguments args can be used
for parameterized queries.
| Example | Result | Comment |
|---|---|---|
query([1,2,3,4,5], '[ _>2 && _<5 ]') |
[3,4] |
|
query([1,2,3,4,5], '[ _>? && _<? ]', 2, 5) |
[3,4] |
same using parameters |
query([1,2,3,4,5], '[ _>6 ]') |
[] |
jsqry.first(target, query[, ...args])
Same as jsqry.query described above but returns first item of result list or null in case of empty result.
| Example | Result |
|---|---|
first([1,2,3,4,5], '[ _>? && _<? ]', 2, 5) |
3 |
first([1,2,3,4,5], '[ _>6 ]') |
null |
Query in general can have a form below
field1.field2[ CONDITION or INDEX or SLICE ].field3{ TRANSFORMATION }.field4<< QUERY >>.field5.x( KEYEXPR )
Here:
| part | meaning |
|---|---|
field1.field2.field3... |
fields access, same as in JS |
[ CONDITION ] |
filtering |
[ INDEX ] |
index access, same as in JS |
[ FROM:TO:STEP ] |
slices, Python-style |
{ TRANSFORMATION } |
object transformation |
.x( KEYEXPR ) |
call action |
<< QUERY >> |
nested filtering |
Note: all mentioned query elements are optional and can be combined in arbitrary order.
| Example | Result |
|---|---|
query([{a:1},{a:2},{a:3}], 'a') |
[1,2,3] |
query([{a:{b:1}},{a:{b:2}},{a:{b:3}}], 'a.b') |
[1,2,3] |
query([{name:"John"},{name:"Peter"}], 'name.length') |
[4,5] |
Filtering has a form [ CONDITION ] where CONDITION should be functional expression.
Let’s elaborate a bit how this works.
In fact, it’s very simple. Every expression during execution by jsqry is substituted to a function this way:
| from | to |
|---|---|
EXPRESSION |
function(_,i) { return EXPRESSION } |
(here _ — the value of item, i — its index).
This function is then applied to the elements being queried.
Examples of filtering:
| Example | Result | Comment |
|---|---|---|
query([1,2,3,4,5], '[ _ % 2 == 1 ]') |
[1,3,5] |
odd elements |
query([{a:1},{a:2},{a:3}], '[_.a>=2]') |
[{a:2},{a:3}] |
|
query(["a", "bb", "aaa", "c"], '[_.startsWith("a")]') |
["a","aaa"] |
|
query(["a", "bb", "aaa", "c"], '[_.length>1]') |
["bb","aaa"] |
|
query(["",1,null,"B",undefined,333,false], '[_]') |
[1, "B", 333] |
only true-like items |
Examples using index argument i:
| Example | Result | Comment |
|---|---|---|
query([1,2,3,4,5,6], '[ i % 2 ]') |
[2,4,6] |
Take every 2nd element * |
query([1,2,3,4,5,6], '[ i > 0 ]') |
[2,3,4,5,6] |
Omit first element * |
* - same result is achievable by slicing: [1::2] and [1:].
Same to JS with addition: index can be negative (meaning index from the end).
| Example | Result | Comment |
|---|---|---|
query(["a", "bb", "aaa", "c"], '[2]') |
["aaa"] |
|
first(["a", "bb", "aaa", "c"], '[2]') |
"aaa" |
|
first(["a", "bb", "aaa", "c"], '[2].length') |
3 |
|
first(["a", "bb", "aaa", "c"], '[-1]') |
"c" |
last element |
This is very similar to Python’s slicing.
Has form of [ FROM:TO:STEP ]. Any of FROM / TO / STEP is optional.
| Example | Result | Comment |
|---|---|---|
query([1,2,3,4,5], '[::2]') |
[1,3,5] |
Every other element |
query([1,2,3,4,5], '[1:]') |
[2,3,4,5] |
All but 1st item |
query([1,2,3,4,5], '[:-1]') |
[1,2,3,4] |
All but last item |
query([1,2,3,4,5], '[::-1]') |
[5,4,3,2,1] |
Reverse |
query([1,2,3,4,5], '[:3]') |
[1,2,3] |
First 3 |
Transformation has a form { TRANSFORMATION } where TRANSFORMATION should be functional expression.
| Example | Result | Comment |
|---|---|---|
query([1,2,3,4,5], '{_*100}') |
[100,200,300,400,500] |
|
query([1,2,3,4,5], '{_*?}', 100) |
[100,200,300,400,500] |
Same using parameter |
query(['a', 'bB', 'Ccc'], '{_.toUpperCase()}') |
["A", "BB", "CCC"] |
|
query(Array(5), '{i}') |
[0, 1, 2, 3, 4] |
Generate number sequence |
query([1,2,3,5],'{?(_,?)}', Math.pow, 2) |
[1, 4, 9, 25] |
squares |
query([{f:'John',l:'Doe'},{f:'Bill',l:'Smith'}], '{_.f + " " + _.l}') |
["John Doe","Bill Smith"] |
Calls are used to apply some transformation to collection as a whole.
At the moment these are supported:
| call | description |
|---|---|
| .s( KEYEXPR ) | sorting |
| .u( KEYEXPR ) | unique |
| .g( KEYEXPR ) | grouping |
Note that any of the call can accept optional functional expression KEYEXPR that will define the behavior of a call.
If omitted the default is used which is identity (_).
| Example | Result | Comment |
|---|---|---|
query([2,3,1,5,4], 's()') |
[1,2,3,4,5] |
sort |
query([2,3,1,5,4], 's(-_)') |
[5,4,3,2,1] |
sort desc |
query([{age:5},{age:1},{age:3}],'s(_.age)') |
[{age:1},{age:3},{age:5}] |
sort by age |
first([{age:5},{age:1},{age:3}],'s(-_.age).age') |
5 |
max age |
first([{age:20,name:'John'},{age:30,name:"Peter"},'s(-_.age).name') |
"Peter" |
oldest |
query([1,2,1,1,3,2], 'u()') |
[1,2,3] |
unique |
query(["aa", "b", "a", "bbb", "c"], 'u(_[0])') |
["aa", "b", "c"] |
unique by 1st letter |
query([1,2,1,1,3,2], 'g()') |
[[1,[1,1,1]],[2,[2,2]],[3,[3]]] |
group |
first([1,2,1,1,3,2], 'g().s(-_[1].length).0') |
1 |
the most popular digit |
query(["aa", "b", "a", "bbb", "c"], 'g(_[0])') |
[["a",["aa","a"]],["b",["b","bbb"]],["c",["c"]]] |
group by 1st letter |
You can define your own call handler myCall by implementing the function for jsqry.fn.myCall.
Let’s implement as an example the partition function of lodash which splits an array into two according to a function
_.partition([1, 2, 3, 4], n => n % 2);
// → [[1, 3], [2, 4]]
Here is how you do this in jsqry using custom call:
jsqry.fn.partition = function (pairs, res) {
const trueElts = [];
const falseElts = [];
res.push(trueElts, falseElts);
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i];
const e = pair[0]; // input element
const v = pair[1]; // function result for it
if (v)
trueElts.push(e);
else
falseElts.push(e);
}
};
query([1, 2, 3, 4], 'partition( _ % 2 )')
// → [[1, 3], [2, 4]]
Nested filtering has a form << QUERY >> where QUERY should be jsqry query string.
Nested filtering can help you in case of a query like select a parent who has a child older 10.
If you try to achieve this using filtering you realize that you need to implement some sort of a loop in condition part:
var parents = [{name:"John", children:[{age:1},{age:5}]}, {name:"Alice", children:[{age:7},{age:12}]}];
first(parents, '[_.children.filter(child=>child.age>10).length].name')
// → "Alice"
And here is how we can do the same much simpler using nested filtering:
first(parents, '<<children[_.age>10]>>.name')
// → "Alice"
During nested filtering element is included if nested query for it yields a result with at least one true-like element.
This will help you if you have array of arrays and you want to query the inner elements.
You need to use * path element.
| Example | Result | Comment |
|---|---|---|
query([["a", "bb"], ["cccc"]],'*') |
["a", "bb", "cccc"] |
|
query([["a", "bb"], ["cccc"]],'*.length') |
[1, 2, 4] |
|
query([["a", "bb"], ["cccc"]],'length') |
[2, 1] |
Compare with previous |
query([["a", "bb"], ["cccc",["dd"]]],'*') |
["a", "bb", "cccc", ["dd"]] |
|
query([["a", "bb"], ["cccc",["dd"]]],'*.*') |
["a", "bb", "cccc", "dd"] |
In any functional expression two implicit functions f (stands for first) and q (stands for query)
are available.
Thus, you can use jsqry right inside your queries.
Example:
const input = [
{ name: "Alice", props: [{ key: "age", val: 30 }, { key: "car", val: "Volvo" }] },
{ name: "Bob", props: [{ key: "age", val: 40 }] },
{ name: "John", props: [] },
];
query(input, '{ _.name + " : " + (f(_.props,"[_.key===`age`].val")||"") }')
// → ["Alice : 30", "Bob : 40", "John : "]
Here are some interesting results you can achieve with jsqry if you twist it a bit :-)
Zip:
query(['a', 'b', 'c', 'd'], '{[_,?[i]]}', ['A', 'B', 'C', 'D'])
// → [['a','A'],['b', 'B'],['c', 'C'],['d', 'D']]
query(['a', 'b', 'c', 'd'], '{ [_, ?[i], ?[i]] }', ['A', 'B', 'C', 'D'], ['AA', 'BB', 'CC', 'DD'])
// → [['a','A', 'AA'],['b', 'B', 'BB'],['c', 'C', 'CC'],['d', 'D', 'DD']]
Enumerate:
query(['a', 'b', 'c', 'd'], '{[i,_]}')
// → [[0, 'a'], [1, 'b'], [2, 'c'], [3, 'd']]
query(Array(26), '{String.fromCharCode(i+97)}').join('')
// → 'abcdefghijklmnopqrstuvwxyz'
Difference:
query([1, 2, 1, 0, 3, 1, 4], '[?.indexOf(_)<0]', [0, 1])
// → [2, 3, 4]
Union:
query([[1, 2, 3], [101, 2, 1, 10], [2, 1]], '*.u()')
// → [1, 2, 3, 101, 10]
Famous FizzBuzz program:
query(Array(20), '{ i++, i % 15 == 0 ?? "FizzBuzz" : i % 3 == 0 ?? "Fizz" : i % 5 == 0 ?? "Buzz" : i }')
// → [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz", 16, 17, "Fizz", 19, "Buzz"]