链接 JQ if: if A then B C else D end

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

我想在 JQ if 语句中交换两个值。

给出以下 JSON:

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

我想使用 JQ 根据条件交换两个对象的计数以产生以下结果。

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

到目前为止我可以用

将第一个水果从0改成1
jq '
  if .food.fruit[] | select(.type=="apple") | .count == 0
  then
    .food.fruit[] | select(.type=="apple") | .count = 1
  else
    empty
  end
'

但我找不到正确的运算符来修改第二个。使用 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
'

我无法将其通过管道传输,因为那样会将单个水果对象通过管道传输到下一行,所以我不确定我应该在此处使用哪个运算符 - 如果甚至支持此类功能。 非常感谢任何帮助!

.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

这是一个高效的解决方案,甚至可能不需要完全遍历 fruit 数组一次。

想法是找到“apple”和“banana”对象的路径,这样交换就可以在没有任何进一步迭代的情况下完成。

为了提高效率,该解决方案使用 foreach 以便在找到“苹果”和“香蕉”对象的路径后跳出(单个)循环。

.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

上述方案可以抽象为:

# 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")

这是一个简单的解决方案,它仅使用内置函数并展示了如何使用“|”将更新链接在一起(*).

.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
  )

更高效的解决方案

这是一个类似的解决方案,它避免了 index 的问题,并且由于不使用 map.

而更加 space 高效
# 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
  )

(*)

内置的 index(在 jq 1.6 和更早版本中)没有有效地实现,虽然是用 C 编写的,但通常速度很快。这是一个算法上有效的实现:

def ix($x):
  label $go
  | foreach .[] as $v (-1; .+1; select($v == $x) | (., break $go)) 
    // null;