jq:如何执行取消合并/多级对象减法,即给定 X 和 Y,找到 Z 使得 X * Z = Y

jq: how to perform unmerge / multi-level object subtraction i.e. given X and Y, find Z such that X * Z = Y

使用jq我们可以使用*轻松合并两个多级对象X和Y:

X='{
    "a": 1,
    "b": 5,
    "c": {
      "a": 3
    }
  }' Y='{
    "d": 2,
    "a": 3,
    "c": {
      "x": 10,
      "y": 11
    }
  }' && Z=`echo "[$X,$Y]"|jq '.[0] * .[1]'` && echo "Z='$Z'"

给我们:

Z='{
  "a": 3,
  "b": 5,
  "c": {
    "a": 3,
    "x": 10,
    "y": 11
  },
  "d": 2
}'

但在我的例子中,我从 X 和 Z 开始并想要计算 Y(这样 X * Y = Z)。如果我们只有标量属性的对象,那么jq X + Y等于Z,我们也可以将Y计算为jq Z - X。但是,如果 X 或 Y 包含具有对象值的属性,如上例所示:

X='{
    "a": 1,
    "b": 5,
    "c": {
      "a": 3
    }
  }' Z='{
  "a": 3,
  "b": 5,
  "c": {
    "a": 3,
    "x": 10,
    "y": 11
  },
  "d": 2
}' && echo "[$X,$Z]" | jq '.[1] - .[0]'

引发错误jq: error (at <stdin>:16): object ({"a":3,"b":...) and object ({"a":1,"b":...) cannot be subtracted

这个问题有jq的优雅解决方案吗?

更新:我接受了我发现更易于阅读/维护且性能卓越的答案。此外,我发现我需要的一个问题是,如果 X 包含 Z 中不存在的键 K,我需要输出 (Y) 通过包含值为 null 的键 K 来使它无效。

我能想到的最好的方法是使用以下方法预处理 Z 以添加丢失的键:

def add_null($y):
    reduce (to_entries[] | [ .key, .value ] ) as [ $k, $v ] (
       $y;
       if $y | has($k) | not then
          .[$k] = null
       elif $v | type == "object" then
          .[$k] = ($v | add_null($y[$k]))
       else
          .[$k] = $v
       end
    );

所以我们最终得到:

def add_null(...);

def remove(...);

. as [ $X, $Z ] | ($X | add_null($Z)) | remove($X)

如有任何对此变体的更好建议,我们仍将不胜感激!

我不知道这是否优雅,但它适用于您的样本数据

echo "[$X,$Z]" | jq '
  . as [$x,$z]
  | map([paths(scalars)])
  | .[0] |= map(select(. as $p | [$x, $z | getpath($p)] | .[1] == .[0]))
  | reduce (.[1] - .[0])[] as $p (null; setpath($p; $z | getpath($p)))  
'
{
  "a": 3,
  "c": {
    "x": 10,
    "y": 11
  },
  "d": 2
}

Demo

def remove($o2):
   reduce ( to_entries[] | [ .key, .value ] ) as [ $k, $v1 ] (
      {};
      if $o2 | has($k) | not then
         # Keep existing value if $o2 doesn't have the key.
         .[$k] = $v1
      else
         $o2[$k] as $v2 |
         if $v1 | type == "object" then
            # We're comparing objects.
            ( $v1 | remove($o2[$k]) ) as $v_diff |
            if $v_diff | length == 0 then
               # Discard identical values.
               .
            else
               # Keep the differences of the values.
               .[$k] = $v_diff
            end
         else
            # We're comparing non-objects.
            if $v1 == $v2 then
               # Discard identical values.
               .
            else
               # Keep existing value if different.
               .[$k] = $v1
            end
         end
      end
   );

. as [ $Z, $X ] | $Z | remove($X)

Demo 在 jqplay

def sub($v2):
   (       type ) as $t1 |
   ( $v2 | type ) as $t2 |
   if $t1 == $t2 then
      if $t1 == "object" then
         with_entries(
            .key as $k |
            .value = (
               .value |
               if $v2 | has($k) then sub( $v2[$k] ) else . end
            )
         ) |
         select( length != 0 )
      else
         select( . != $v2 )
      end
   else
      .
   end;

. as [ $Z, $X ] | $Z | sub($X)

Demo 在 jqplay