Chaining JQ if: if A then B C else D end

Viewed 121

I would like to swap two values in a JQ if statement.

Given the following JSON:

{
  "food": {
    "fruit": [
      {
        "type": "apple",
        "count": 0
      },
      {
        "type": "banana",
        "count": 1
      },
      {
        "type": "orange",
        "count": 0
      }
    ]
  }
}

I want to swap the counts of the two objects based on a condition to result in the following, using JQ.

{
  "food": {
    "fruit": [
      {
        "type": "apple",
        "count": 1
      },
      {
        "type": "banana",
        "count": 0
      },
      {
        "type": "orange",
        "count": 0
      }
    ]
  }
}

So far I can correct the first fruit from 0 to 1 with

jq '
  if .food.fruit[] | select(.type=="apple") | .count == 0
  then
    .food.fruit[] | select(.type=="apple") | .count = 1
  else
    empty
  end
'

but I can't get the correct operator to modify the second. Is something like the following possible, using JQ?

jq '
  if .food.fruit[] | select(.type=="apple") | .count == 0
  then
    .food.fruit[] | select(.type=="apple") | .count = 1 &
    .food.fruit[] | select(.type=="banana") | .count = 0
  else
    empty
  end
'

I can't pipe it as that would pipe the single fruit object to the next line, so I'm not sure which operator I'm supposed to use here - if such functionality is even supported. Any help is greatly appreciated!

3 Answers

Here is an efficient solution that may not even need to traverse the fruit array once completely.

The idea is to find the paths to the "apple" and "banana" objects, so that the swap can be done without any further iteration.

For additional efficiency, the solution uses foreach in order to break out of the (single) loop once the paths to the "apple" and "banana" objects have been found.

.food.fruit |=
  ( . as $fruits
   | label $go
   | (foreach paths(objects) as $p ({}; 
      ($fruits|getpath($p)) as $x
      | if  ($x|.type)=="apple"
        then .apple  = {path: ($p + ["count"]), count: $x.count}
        elif ($x|.type) == "banana"
        then .banana = {path: ($p + ["count"]), count: $x.count}
        else . end;
      if .apple and .banana then ., break $go else empty end)
     ) as $dict
   | if $dict | .apple and .banana
     then setpath( $dict.apple.path;  $dict.banana.count) 
     |    setpath( $dict.banana.path; $dict.apple.count)
     else . end
 )

def swap/4

The above solution can be abstracted as follows:

# Input: an array of JSON objects.
#
# In the objects with .[$f1key] == $f1 and .[$f1key] == $f2 if any,
# swap the values at .[$gkey].
# If the the array has more than one object with .[$f1key] == $f1,
# then the last one will be selected, and similarly with respect to $f2
#
def swap($f1; $f2; $fkey; $gkey):
  . as $in
  | label $go
  | (foreach paths(objects) as $p ({}; 
      ($in|getpath($p)) as $x
      | if  ($x[$fkey])==$f1
        then .f1 = {path: ($p + [$gkey]), count: $x[$gkey]}
        elif ($x[$fkey]) == $f2
        then .f2 = {path: ($p + [$gkey]), count: $x[$gkey]}
        else . end;
      if .f1 and .f2 then ., break $go else empty end)
    ) as $dict
  | if $dict | .f1 and .f2
    then setpath( $dict.f1.path; $dict.f2.count) 
    |    setpath( $dict.f2.path; $dict.f1.count)
    else . end ;

.food.fruit |= swap("apple"; "banana"; "type"; "count")

Here is a straightforward solution that only uses builtins and shows how updates can be chained together using '|' (*).

.food.fruit |=
  (map(.type) as $types
   | ($types | index("apple")) as $ia
   | ($types | index("banana")) as $ib
   | .[$ia].count as $ac
   | .[$ia].count = .[$ib].count
   | .[$ib].count = $ac
  )

More efficient solution

Here is a similar solution that avoids the issues with index and is more space-efficient as it does not use map.

# index of $needle in a stream
def index_of($needle; stream):
  label $go
  | foreach stream as $x (-1; .+1; select($x==$needle) | (., break $go))
    // null;

.food.fruit |=
  (  index_of("apple";  .[].type) as $ia
   | index_of("banana"; .[].type) as $ib
   | .[$ia].count as $ac
   | .[$ia].count = .[$ib].count
   | .[$ib].count = $ac
  )

(*)

The built-in index is (in jq 1.6 and earlier) not implemented efficiently, though being written in C, it typically is fast. Here is an algorithmically efficient implementation:

def ix($x):
  label $go
  | foreach .[] as $v (-1; .+1; select($v == $x) | (., break $go)) 
    // null;
.food.fruit |= (
   ( .[] | select( .type == "apple"  ) | .count ) as $count1 |
   ( .[] | select( .type == "banana" ) | .count ) as $count2 |

   ( .[] | select( .type == "apple"  ) | .count ) = $count2 |
   ( .[] | select( .type == "banana" ) | .count ) = $count1
)

jq play


.food.fruit |= (
   ( .[] | select( .type == "apple"  ) | .count ) as $count1 |
   ( .[] | select( .type == "banana" ) | .count ) as $count2 |

   .[] |= (
      .count = (
         if .type == "apple" then
            $count2
         elif .type == "banana" then
            $count1
         else
            .count
         end
      )
   )
)

jq play

Related