'How to sum up string fields that contain percentages with 'jq'?

I have a JSON file that's keeping track of column widths for a table as percentages. So the input file, columns.json, looks something like this:

[
  {
    "name": "Column A",
    "width": "33%"
  },
  {
    "name": "Column B",
    "width": "33%"
  },
  {
    "name": "Column C",
    "width": "33%"
  },
  {
    "name": "Column D",
    "visible": false
  }
]

Some columns are not displayed and therefore don't have widths (jq '.[].width' will return nulls for these), and then there's also the issue of the percent signs. Otherwise I might've used munge | munge | paste -sd+ | bc, which is usually what I use for summing things up in the shell, but that seems stupid here because jq ought to be able to do this by itself.

So using only jq, how can I sum up the width fields from this file, e.g., to make sure they don't exceed 100%?

Things I have tried (that didn't work)

I use select(.) here to filter out records that don't have a .width, then get rid of the percent sign:

jq '[.[].width | select(.) | sub("%"; "")] | add' columns.json

…but that just concatenates the strings and returns "333333".

I didn't see any mention of the word "typecast" in the jq man page, so I thought maybe it would do type inference, treating a string that looks like a number as a number in the right context:

jq '[.[].width | select(.) | sub("%"; "") | .+0] | add' columns.json

…but that just yields and error message like:

jq: error (at columns.json:18): string ("33") and number (0) cannot be added


Solution 1:[1]

This SO answer gave me the clue that there was a tostring function, so a more thorough search of the manual page revealed that the analogous function for numbers is tonumber.

Well, duh. I guess I was expecting it to be named something else, like toint, which is why I didn't find it while string-searching through the man page.

Here is the solution I ended up with:

jq 'map(.width | sub("%"; "")? | tonumber) | add' columns.json

Instead of select(.) to filter out the nulls from objects with no .width field, I just silently ignore the error in sub (with ?), which drops those records.

Note that map(.width) is just another way of saying [.[].width].

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1