Reputation: 3387
Does jq have an exclusive or (AKA xor) operator? I'm having trouble finding it in its documentation.
(I've had trouble finding other topics in the docs that I later found worked in jq and are documented. I probably didn't find them at first because I'd used poor search terms.)
I wanted jq to return all top-level objects from the input that have array properties containing one of two specific values, but not both values.
For example, given the input:
[
{"letters": ["a", "c"]},
{"letters": ["a", "b", "c"]},
{"letters": ["b", "c"]}
]
I want only the objects whose "letters"
property contains either "a"
or "b"
, but not both.
I ended up using the long-winded filter:
map(select(.letters//[]|((contains(["a"]) or contains(["b"])) and (contains(["a", "b"])|not))))
Which gave the correct output:
[{"letters":["a","c"]},{"letters":["b","c"]}]
But that's long, tedious, and a maintenance headache. Is there a simpler way to do accomplish this?
A "jq play" snippet for this code: https://jqplay.org/s/mwBhsYud2F
PS: Even if there isn't any better solution than the one I've found, I'll be happy to receive constructive criticism about improving it.
Upvotes: 1
Views: 684
Reputation: 116700
xor
can easily be defined:
def xor($a;$b): ($a or $b) and (($a and $b)|not);
contains
is tricky (it is probably better not to use it unless you have studied its subtleties). In general, it would be better to use index
:
.letters | select( xor( index("a"); index("b") ))
For efficiency, it would be even better to use IN
if your jq has it:
.letters as $a | select( xor( "a" | IN($a[]); "b" | IN($a[]) ))
Upvotes: 1
Reputation: 14645
How about this: if data.json
contains your sample data the command
$ jq -Mc '
def xor($a;$b): $a != $b ;
map(select(.letters|xor(contains(["a"]);contains(["b"]))))
' data.json
produces
[{"letters":["a","c"]},{"letters":["b","c"]}]
Note that the xor
used above works here but isn't safe to use with non-boolean parameters. A more robust version is:
def xor($a;$b): ($a|not) != ($b|not) ;
Upvotes: 0
Reputation: 14645
If there are no duplicates in .letters
, here is another way which takes advantage of a little-known use of array indexing:
$ jq -Mc 'map(select(.letters|.[["a"]]+.[["b"]]|length==1))' data.json
produces
[{"letters":["a","c"]},{"letters":["b","c"]}]
This works because if $x
is an array $x[ ["a"] ]
returns the indices of "a"
within $x
. e.g.
$ jq -Mnc '["a","c"][["a"]]'
[0]
$ jq -Mnc '["a","c"][["b"]]'
[]
$ jq -Mnc '["a","c","a","b"][["a"]]'
[0,2]
See also jq indices builtin (which uses it in its implementation)
This may be too tricky to recommend using in the general case compared to a more obvious approach and if .letters
contains duplicates then the simplistic length==1
check won't work.
Upvotes: 0