1

I see summing value in an array of maps and wonder if there is an elegant way in jq to perform a general recursion over a set of objects and sum all values they contain to produce a single output object as the sum of all the other objects.

If it is easier to do this over a single input object which wraps all the other objects (to avoid slurping), that is fine too.

  • recurse over a slurped series of independent objects which have a lot of arbitrary nesting (arbitrary in the sense that I don't want to have to code jq for the specifics of the hierarchy of objects if general recursion can do what I want), and
  • sum every numeric value of a key:value pair found at any level

Each object can have be sparse, but all coincident object key paths should sum their values to the same location in the single output object hierarchy.

For example (wrap an array around these 3 objects if that makes it easier):

{ A { B { x:1, y:4      }, C { x:3,      z:6 }, t:2 }, s:5 }
{ A { B { x:2     , z:3 },                      t:1 }      }
{ A {                    , C {      y:3, z:2 }      }, s:3 }

would produce

{ A { B { x:3, y:4, z:3 }, C { x:3, y:3, z:8 }, t:3 }, s:8 }
Community
  • 1
  • 1
redgiant
  • 474
  • 5
  • 14

2 Answers2

1

TL:DR; Over an array of these objects, do . as $dot | reduce ([$dot[] | paths(numbers)] | unique)[] as $path ({}; setpath($path; [$dot[] | getpath($path)] | add))


Since your example input is not valid JSON, here's the JSON I used for testing this. Let me know if this doesn't match with yours:

[
    {"a": {"b": 1, "c": 2},         "e": 3},
    {"a": {        "c": 2, "d": 3},         "f": 4}
]

First, we'll look at how to perform this "additive merge" operation between two objects; afterwards, it should be easy to perform it over an array of objects.

We'll define our function as additive_merge($xs; $ys). First, we'll get a list of all the paths on each of these objects in which there are numbers, with duplicates removed:

([$xs, $ys | paths(numbers)] | unique) as $paths

Then, we'll reduce this list of paths, with an empty object as the initial state, using setpath to set each path in the empty object to the sum of the values of those paths on $xs and $ys:

reduce $paths[] as $path ({}; setpath($path; [$xs, $ys | getpath($path)] | add))

Now, all together, we have our additive_merge function:

def additive_merge($xs;$ys): reduce ([$xs, $ys | paths(numbers)] | unique)[] as $path ({}; setpath($path; [$xs, $ys | getpath($path)] | add));

If we want to run this function over an array, we can either reduce the array over this function:

reduce .[] as $x ({}; additive_merge(.; $x))

Or modify the function so that it works over an array of objects, which is actually really easy; just use the dot input, save it to a variable and decompress it wherever $xs, $ys was used on the previous function:

def additive_merge: . as $dot | reduce ([$dot[] | paths(numbers)] | unique)[] as $path ({}; setpath($path; [$dot[] | getpath($path)] | add));

Hope this helps!

  • Great, thank you! I use it like this to feed a set of periodic summaries already produced and meld them together via summing all relevant values. `def additive_merge($xs;$ys): reduce ([$xs, $ys | paths(numbers)] | unique)[] as $path ({}; setpath($path; [$xs, $ys | getpath($path)] | add)); reduce inputs as $f ({}; additive_merge(.; $f)) ` – redgiant Jul 18 '16 at 22:31
  • Oh, yeah, that works too! The advantage of the two-element version over the array version is that you don't need to analyze the entire array of objects beforehand, so you can use it with something like `inputs`. Great use case! –  Jul 19 '16 at 13:30
0

Here is a solution that uses tostream, select, reduce, setpath and getpath.

reduce (tostream|select(length==2)|.[0] = .[0][1:]) as [$p,$v] (
   {};
   setpath($p; getpath($p) + $v)
)
jq170727
  • 13,159
  • 3
  • 46
  • 56