Mr. Lance E Sloan
Mr. Lance E Sloan

Reputation: 3387

exclusive or (xor) operator?

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

Answers (3)

peak
peak

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

jq170727
jq170727

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

jq170727
jq170727

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

Related Questions