Invisible999
Invisible999

Reputation: 577

Jq - calculate length of each array in JSON and update it

I have the following JSON array

[
  {
    "name": "California",
    "Data": {
      "AB00001": ["Los Angeles", "San Francisco", "Sacramento", "Fresno", "San Jose", "Palo Alto"]
    }
  },
  {
    "name": "Oregon",
    "Data": {
      "CD00002": ["Portland", "Salem", "Hillsboro"]
    }
  },
  {
    "name": "Washington",
    "Data": {
      "EF00003": ["Seattle", "Tacoma", "Spokane", "Bellevue"]
    }
  }
]

With jq '.[].Data[] | length' I can get the length of each array, but I need to create Length key under Data object and assign to it the length of the array which is in the Data object. The result should look like the following:

[
    {
        "name": "California",
        "Data": {
            "ID00001": ["Los Angeles", "San Francisco", "Sacramento", "Fresno", "San Jose", "Palo Alto"],
            "Length": 6
        }
    },
    {
        "name": "Oregon",
        "Data": {
            "ID00002": ["Portland", "Salem", "Hillsboro"],
            "Length": 3
        }
    },
    {
        "name": "Washington",
        "Data": {
            "ID00003": ["Seattle", "Tacoma", "Spokane", "Bellevue"],
            "Length": 4
        }
    }
]

The problem here is that in my example the object name holding the array (Data in my example) and the array name itself (AB00001/CD00002/EF00003) are not known in advance. However, the values of the name key is known. Also, the array might be empty, so in this case, the Length should be 0.

So the algorithm pseudocode should be either of one as I've envisioned:

for the whole JSON file, 
if the type is an array, 
then assign it to the Length key created in the parent object of that array, 
next

or

For the specific value in the name key, select,
if the entry contains an array
create Length key in the array's parent object, 
assign the length of the array,
select the next value of the name key

I tried to use with jq's walk or .. for the first approach but didn't work. What are the alternatives?

Upvotes: 8

Views: 14042

Answers (2)

peak
peak

Reputation: 116919

A solution to the immediate problem (in which .Data is a single-key object with an array-valued key) could be as simple as:

map( .Data.Length = (.Data[]|length) )

This can be adapted to the more general problem in steps. First, consider this generalization:

map( .Data |= if length==1 and (.[]|type) == "array" 
              then .Length = (.[]|length) 
              else . end )

Solution

To make the actual solution easier to understand, let's define a helper function:

def addLength:
   (first(keys_unsorted[] as $k
          | select(.[$k]|type == "object")
          | .[$k]
          | keys_unsorted[] as $l 
          | select(.[$l]|type == "array")
          | [$k,$l]) // null) as $a
   | if $a 
     then setpath([$a[0],"Length"]; getpath($a)|length)
     else .
     end;

A general solution can now be written using walk:

walk(if type == "object" and has("name")
     then addLength
     else . end)

Upvotes: 10

martin8guest
martin8guest

Reputation: 31

This seems to be what you want:

jq '.[].Data |= . + { length:.[]|length } ' data.json

Output:

[
  {
    "name": "California",
    "Data": {
      "AB00001": [
        "Los Angeles",
        "San Francisco",
        "Sacramento",
        "Fresno",
        "San Jose",
        "Palo Alto"
      ],
      "length": 6
    }
  },
  {
    "name": "Oregon",
    "Data": {
      "CD00002": [
        "Portland",
        "Salem",
        "Hillsboro"
      ],
      "length": 3
    }
  },
 # etc.

Upvotes: 3

Related Questions