jq: how to only update data if existing? - json

With dev version of jq, this could be done with jq '.x.y |= if . then 123 else empty end'. (Because bug #13134 is solved.)
How can I do this in jq 1.5?
example:
in {"x": {"y": 5}}, y should be changed to 123,
but in {"x": {"z": 9}}, nothing should change.

do you need to use |=? If not could you use ordinary assignment? e.g.
jq -Mnc '
{"x": {"y": 5}} | if .x.y != null then .x.y = 123 else . end
, {"x": {"z": 9}} | if .x.y != null then .x.y = 123 else . end
'
output
{"x":{"y":123}}
{"x":{"z":9}}

With built-in has() function:
jq -nc '{"x":{"y": 5}} | if (.x | has("y")) then .x.y=123 else empty end'
The output:
{"x":{"y":123}}

Both the following produce the desired results (whether using 1.5 or later), but there are important differences in the semantics (having to do with the difference between {"x": null} and {}):
if has("x") and (.x | has("y")) then .x.y = 123 else . end
if .x.y? then .x.y = 123 else . end

Using streams could actually handle this quite nicely. A stream for an object will yield paths and values to actual existing values in your input. So search for the pairs that contain your path and update the value while rebuilding the stream.
$ jq --argjson path '["x","y"]' --argjson new '123' '
fromstream(tostream|select(length == 2 and .[0] == $path)[1] = $new)
' input.json

Related

Calculate accumulated times from json with bash script

I have data in json format that logs timestamps (hh:mm in 24h format) with an event (In/Out). My goal is to add up all the time differences between an "IN" event and the next "OUT" event.
For simplification I assume that there are no inconsistencies (The first element is always an "IN" and each "IN" is followed by an "OUT"). Exception: If the last element is an "IN", the calculation has to be done between the current time and the timestamp from the last "IN" event.
This is my script so far, which calculates all the timespans, also between an OUT and an IN event. But I need only those that are inbetween an IN and OUT event.
Any tips what might be more useful here are welcome !
#!/bin/bash
JSON='{ "times": [ [ "7:43", "IN" ], [ "8:26", "OUT" ], [ "8:27", "IN" ], [ "9:12", "OUT" ], [ "9:14", "IN" ], [ "9:22", "OUT" ], [ "9:23", "IN " ], [ "12:12", "OUT" ], [ "13:12", "IN" ] ]}'
IN_TIMES=$(jq '.times | to_entries | .[] | select(.value[1]| tostring | contains("IN")) | .value[0]' <<< "$JSON")
OUT_TIMES=$(jq '.times | to_entries | .[] | select(.value[1]| tostring | contains("OUT")) | .value[0]' <<< "$JSON")
ALL_TIMES=$(jq -r '.times| to_entries | .[] | .value[0]' <<< "$JSON")
prevtime=0
count=0
for i in $(echo $ALL_TIMES | sed "s/ / /g")
do
if [[ "$count" -eq 0 ]]; then
(( count++ ))
prevtime=$i
continue
else
(( count++ ))
fi
time1=`date +%s -d ${prevtime}`
time2=`date +%s -d ${i}`
diffsec=`expr ${time2} - ${time1}`
echo From $prevtime to $i: `date +%H:%M -ud #${diffsec}`
prevtime=$i
done
Here's an only-jq solution that only calls jq once.
Please note, though, that it may need tweaking to take into account time zone considerations, error-handling, and potentially other complications:
def mins: split(":") | map(tonumber) | .[0] * 60 + .[1];
def diff: (.[1] - .[0]) | if . >= 0 then . else 24*60 + . end;
def now_mins: now | gmtime | .[3] * 60 + .[4];
def pairs:
range(0; length; 2) as $i | [.[$i], .[$i+1] ];
def sigma(s): reduce s as $s (0; . + $s);
.times
| map( .[0] |= mins )
| if .[-1][1] == "IN" then . + [ [now_mins, "OUT"] ] else . end
| sigma(pairs | map(.[0]) | diff)
Since you measure times up to the minute, it is enough to compute minutes without messing up with the command date. I have an awk solution:
awk -F: -vIRS=" " -vfmt="From %5s to %5s: %4u minutes\n" \
'{this=$1*60+$2}a{printf(fmt,at,$0,this-a);a=0;next}{a=this;at=$0}\
END{if(a){$0=strftime("%H:%M");printf(fmt,at,$0,$1*60+$2-a)}}' <<<"$ALL_TIMES"
which works by defining a colon as field separator and a space as record separator. In this way we get a separate record with two fields for each time. Then
{this=$1*60+$2} : We compute how many minutes there are in the current record and put them in the variable this.
a{printf(fmt,at,$0,this-a);a=0;next} : If the (initially empty) variable a is not null nor zero, we are reading an OUT entry, so we print what we want, set a to zero because the next field will be an IN entry, and we continue to the next record.
{a=this;at=$0} : Otherwise, we are reading an IN entry, and set a to its minutes and at to its string representation (needed we will print it, as per previous case).
END{if(a){$0=strftime("%H:%M");printf(fmt,at,$0,$1*60+$2-a)}} : at the end, if we still have some dangling IN data, we set $0 to be the properly formatted current time and print what we want.
All done.
With Xidel and a little XQuery magic this is rather simple:
#!/bin/bash
JSON='{"times": [["7:43", "IN"], ["8:26", "OUT"], ["8:27", "IN"], ["9:12", "OUT"], ["9:14", "IN"], ["9:22", "OUT"], ["9:23", "IN "], ["12:12", "OUT"], ["13:12", "IN"]]}'
xidel -s - --xquery '
let $in:=$json/(times)()[contains(.,"IN")](1) ! time(
substring(
"00:00:00",
1,
8-string-length(.)
)||.
),
$out:=$json/(times)()[contains(.,"OUT")](1) ! time(
substring(
"00:00:00",
1,
8-string-length(.)
)||.
)
for $x at $i in $out return
concat(
"From ",
$in[$i],
" to ",
$x,
": ",
$x - $in[$i] + time("00:00:00")
)
' <<< "$JSON"
$in:
00:07:43
00:08:27
00:09:14
00:09:23
00:13:12
$out:
00:08:26
00:09:12
00:09:22
00:12:12
Output:
From 00:07:43 to 00:08:26: 00:00:43
From 00:08:27 to 00:09:12: 00:00:45
From 00:09:14 to 00:09:22: 00:00:08
From 00:09:23 to 00:12:12: 00:02:49

Spread number equally across elements of array (and add remainder to beginning of ring)

Let's say I have some JSON array, we'll call it A:
["foo", "bar", "baz"]
And I have some number X, let's say 5 in this case.
I want to produce the following object in jq:
{
"foo": 2,
"bar": 2,
"baz": 1,
}
This is the number 5 divided up equally across the elements of the array, with the remainder being distributed to the elements at the beginning of the ring. You could maybe think of it this way, the value for element N should be ceil(X / length(A)) if index(N) < (X % length(A)), otherwise it should be floor(X / length(A)).
Assuming A is my file input to jq, and I have X defined as a variable, how can I express this in jq?
I have tried 'length as $len | .[] | if index(.) < (5 % $len) then (5 / $len) + 1 else 5 / $len end' | 5 as a starting point but I get 2 for each element.
You can use the transpose function to help build this. It's simpler with a ceil function, which we have to define ourselves. The mapping you are looking for from index to allocation is ceil($count - $i)/$n), where $count is the amount you are distributing, $i is the index in the original list, and $n is the length of the list.
Comments show how each piece works on your sample input of ["foo", "bar", "baz"].
def ceil(v): -(-v | floor);
def objectify(n): {key: .[0], value: ceil(($count - .[1])/n)};
# ["foo", 0] | objectify(3) -> {"key": "foo", "value", 2}
length as $n | # n == 3
[., keys] | # [["foo", "bar", "baz"], [0,1,2]]
[transpose[] | # [["foo", 0], ["bar", 1], ["baz", 2]]
objectify($n)
] |
from_entries # {"foo": 2, "bar": 2, "baz": 1}
Without the comments...
def ceil(v): -(-v | floor);
def objectify(n): {key: .[0], value: ceil(($count - .[1])/n)};
length as $n | [., keys] | [transpose[] | objectify($n)] | from_entries
An example of its use, assuming you saved it to file named distribute.jq:
jq --argjson count 5 -f distribute.jq tmp.json
I found a solution by saving the original input as a variable so that I can continue to reference it while operating on its values.
. as $arr
| length as $len
| [
.[]
| . as $i
| {
$(i): (
if ($arr | index($i)) < ($x % $len) then
($x / $len) + 1
else
$x / $len
end
| floor
)
}
]
| add
The following worked for me with passing --argjson count $X and feeding the array as my input.

jq split string and assign

I have the following json
{
"version" : "0.1.2",
"basePath" : "/"
}
and the desired output is
{
"version" : "0.1.2",
"basePath" : "beta1"
}
I have the following jq which is producing the error below:
.basePath = .version | split(".") as $version | if $version[0] == "0" then "beta"+ $version[1] else $version[0] end
jq: error (at :3): split input and separator must be strings
exit status 5
Using .basePath = .version assigns the value successfully and .version | split(".") as $version | if $version[0] == "0" then "beta"+ $version[1] else $version[0] end on its own returns "beta1". Is there a way to assign the string to the basePath key?
Good news! Your proposed solution is just missing a pair of parentheses. Also, there is no need for $version. That is, this will do it:
.basePath = (.version | split(".")
| if .[0] == "0" then "beta"+ .[1] else .[0] end)

How to print out the top-level json after modification of descendants

Hello i managed to create this jq filter .profiles | recurse | .gameDir? | if type == "null" then "" else . end | scan("{REPLACE}.*") | sub("{REPLACE}"; "{REPLACESTRINGHERE}"). it succesfully replaces what i want (checked at jqplay.org) but now i'd like to print the full json and not just the modified strings
Adapting your query:
.profiles |= walk( if type == "object" and has("gameDir")
then .gameDir |=
(if type == "null" then "" else . end
| scan("{REPLACE}.*") | sub("{REPLACE}"; "{REPLACESTRINGHERE}"))
else .
end )
(This can easily be tweaked for greater efficiency.)
If your jq does not have walk, you can google it (jq “def walk”) or snarf its def from the jq FAQ https://github.com/stedolan/jq/wiki/FAQ
walk-free approach
For the record, here's an illustration of a walk-free approach using paths. The following also makes some changes in the computation of the replacement string -- notably it eliminates the use of scan -- so it is not logically equivalent, but is likely to be more useful as well as more efficient.
.profiles |=
( . as $in
| reduce (paths | select(.[-1] == "gameDir")) as $path ($in;
($in | getpath($path)
| if type == "null" then ""
else sub(".*{REPLACE}"; "{REPLACESTRINGHERE}")
end) as $value
| setpath($path; $value) ))

How to make paths to leafs of a JSON?

Say we have the following JSON:
[
{
"dir-1": [
"file-1.1",
"file-1.2"
]
},
"dir-1",
{
"dir-2": [
"file-2.1"
]
}
]
And we want to get the next output:
"dir-1/file-1.1"
"dir-1/file-1.2"
"dir-1"
"dir-2/file-2.1"
i.e. to get the paths to all leafs, joining items with /. Is there a way to do that on JQ?
I tried something like this:
cat source-file | jq 'path(..) | [ .[] | tostring ] | join("/")'
But it doesn't produce what I need even close.
You could take advantage of how streams work by merging the path with their values. Streams will only emit path, value pairs for leaf values. Just ignore the numbered indices.
$ jq --stream '
select(length == 2) | [(.[0][] | select(strings)), .[1]] | join("/")
' source-file
returns:
"dir-1/file-1.1"
"dir-1/file-1.2"
"dir-1"
"dir-2/file-2.1"
Here is a solution similar to Jeff Mercado's which uses tostream and flatten
tostream | select(length==2) | .[0] |= map(strings) | flatten | join("/")
Try it online at jqplay.org
Another way is to use a recursive function to walk the input such as
def slashpaths($p):
def concat($p;$k): if $p=="" then $k else "\($p)/\($k)" end;
if type=="array" then .[] | slashpaths($p)
elif type=="object" then
keys_unsorted[] as $k
| .[$k] | slashpaths(concat($p;$k))
else concat($p;.) end;
slashpaths("")
Try it online at tio.run!
Using --stream is good but the following is perhaps less esoteric:
paths(scalars) as $p
| getpath($p) as $v
| ($p | map(strings) + [$v])
| join("/")
(If using jq 1.4 or earlier, and if any of the leaves might be numeric or boolean or null, then [$v] above should be replaced by [$v|tostring].)
Whether the result should be regarded as "paths to leaves" is another matter...