def halt_error: halt_error(5); def error: error(.); def map(f): [.[] | f]; def select(f): if f then . else empty end; def sort_by(f): _sort_by_impl(map([f])); def group_by(f): _group_by_impl(map([f])); def unique: group_by(.) | map(.[0]); def unique_by(f): group_by(f) | map(.[0]); def max_by(f): _max_by_impl(map([f])); def min_by(f): _min_by_impl(map([f])); def add: reduce .[] as $x (null; . + $x); def del(f): delpaths([path(f)]); def _assign(paths; value): value as $v | reduce path(paths) as $p (.; setpath($p; $v)); def _modify(paths; update): reduce path(paths) as $p (.; label $out | (setpath($p; getpath($p) | update) | ., break $out), delpaths([$p])); def map_values(f): .[] |= f; # recurse def recurse(f): def r: ., (f | r); r; def recurse(f; cond): def r: ., (f | select(cond) | r); r; def recurse: recurse(.[]?); def recurse_down: recurse; def to_entries: [keys_unsorted[] as $k | {key: $k, value: .[$k]}]; def from_entries: map({(.key // .Key // .name // .Name): (if has("value") then .value else .Value end)}) | add | .//={}; def with_entries(f): to_entries | map(f) | from_entries; def reverse: [.[length - 1 - range(0;length)]]; def indices($i): if type == "array" and ($i|type) == "array" then .[$i] elif type == "array" then .[[$i]] elif type == "string" and ($i|type) == "string" then _strindices($i) else .[$i] end; def index($i): indices($i) | .[0]; # TODO: optimize def rindex($i): indices($i) | .[-1:][0]; # TODO: optimize def paths: path(recurse(if (type|. == "array" or . == "object") then .[] else empty end))|select(length > 0); def paths(node_filter): . as $dot|paths|select(. as $p|$dot|getpath($p)|node_filter); def any(generator; condition): [label $out | foreach generator as $i (false; if . then break $out elif $i | condition then true else . end; if . then . else empty end)] | length == 1; def any(condition): any(.[]; condition); def any: any(.); def all(generator; condition): [label $out | foreach generator as $i (true; if .|not then break $out elif $i | condition then . else false end; if .|not then . else empty end)] | length == 0; def all(condition): all(.[]; condition); def all: all(.); def isfinite: type == "number" and (isinfinite | not); def arrays: select(type == "array"); def objects: select(type == "object"); def iterables: arrays, objects; def booleans: select(type == "boolean"); def numbers: select(type == "number"); def normals: select(isnormal); def finites: select(isfinite); def strings: select(type == "string"); def nulls: select(type == "null"); def values: select(. != null); def scalars: select(. == null or . == true or . == false or type == "number" or type == "string"); def scalars_or_empty: select(. == null or . == true or . == false or type == "number" or type == "string" or ((type=="array" or type=="object") and length==0)); def leaf_paths: paths(scalars); def join($x): reduce .[] as $i (null; (if .==null then "" else .+$x end) + ($i | if type=="boolean" or type=="number" then tostring else .//"" end) ) // ""; def _flatten($x): reduce .[] as $i ([]; if $i | type == "array" and $x != 0 then . + ($i | _flatten($x-1)) else . + [$i] end); def flatten($x): if $x < 0 then error("flatten depth must not be negative") else _flatten($x) end; def flatten: _flatten(-1); def range($x): range(0;$x); def fromdateiso8601: strptime("%Y-%m-%dT%H:%M:%SZ")|mktime; def todateiso8601: strftime("%Y-%m-%dT%H:%M:%SZ"); def fromdate: fromdateiso8601; def todate: todateiso8601; def match(re; mode): _match_impl(re; mode; false)|.[]; def match($val): ($val|type) as $vt | if $vt == "string" then match($val; null) elif $vt == "array" and ($val | length) > 1 then match($val[0]; $val[1]) elif $vt == "array" and ($val | length) > 0 then match($val[0]; null) else error( $vt + " not a string or array") end; def test(re; mode): _match_impl(re; mode; true); def test($val): ($val|type) as $vt | if $vt == "string" then test($val; null) elif $vt == "array" and ($val | length) > 1 then test($val[0]; $val[1]) elif $vt == "array" and ($val | length) > 0 then test($val[0]; null) else error( $vt + " not a string or array") end; def capture(re; mods): match(re; mods) | reduce ( .captures | .[] | select(.name != null) | { (.name) : .string } ) as $pair ({}; . + $pair); def capture($val): ($val|type) as $vt | if $vt == "string" then capture($val; null) elif $vt == "array" and ($val | length) > 1 then capture($val[0]; $val[1]) elif $vt == "array" and ($val | length) > 0 then capture($val[0]; null) else error( $vt + " not a string or array") end; def scan(re): match(re; "g") | if (.captures|length > 0) then [ .captures | .[] | .string ] else .string end ; # # If input is an array, then emit a stream of successive subarrays of length n (or less), # and similarly for strings. def _nwise(a; $n): if a|length <= $n then a else a[0:$n] , _nwise(a[$n:]; $n) end; def _nwise($n): _nwise(.; $n); # # splits/1 produces a stream; split/1 is retained for backward compatibility. def splits($re; flags): . as $s # # multiple occurrences of "g" are acceptable | [ match($re; "g" + flags) | (.offset, .offset + .length) ] | [0] + . +[$s|length] | _nwise(2) | $s[.[0]:.[1] ] ; def splits($re): splits($re; null); # # split emits an array for backward compatibility def split($re; flags): [ splits($re; flags) ]; # # If s contains capture variables, then create a capture object and pipe it to s def sub($re; s): . as $in | [match($re)] | if length == 0 then $in else .[0] | . as $r # # create the "capture" object: | reduce ( $r | .captures | .[] | select(.name != null) | { (.name) : .string } ) as $pair ({}; . + $pair) | $in[0:$r.offset] + s + $in[$r.offset+$r.length:] end ; # # If s contains capture variables, then create a capture object and pipe it to s def sub($re; s; flags): def subg: [explode[] | select(. != 103)] | implode; # "fla" should be flags with all occurrences of g removed; gs should be non-nil if flags has a g def sub1(fla; gs): def mysub: . as $in | [match($re; fla)] | if length == 0 then $in else .[0] as $edit | ($edit | .offset + .length) as $len # create the "capture" object: | reduce ( $edit | .captures | .[] | select(.name != null) | { (.name) : .string } ) as $pair ({}; . + $pair) | $in[0:$edit.offset] + s + ($in[$len:] | if length > 0 and gs then mysub else . end) end ; mysub ; (flags | index("g")) as $gs | (flags | if $gs then subg else . end) as $fla | sub1($fla; $gs); # def sub($re; s): sub($re; s; ""); # repeated substitution of re (which may contain named captures) def gsub($re; s; flags): sub($re; s; flags + "g"); def gsub($re; s): sub($re; s; "g"); ######################################################################## # range/3, with a `by` expression argument def range($init; $upto; $by): def _range: if ($by > 0 and . < $upto) or ($by < 0 and . > $upto) then ., ((.+$by)|_range) else . end; if $by == 0 then $init else $init|_range end | select(($by > 0 and . < $upto) or ($by < 0 and . > $upto)); # generic iterator/generator def while(cond; update): def _while: if cond then ., (update | _while) else empty end; _while; def until(cond; next): def _until: if cond then . else (next|_until) end; _until; def limit($n; exp): if $n < 0 then exp else label $out | foreach exp as $item ([$n, null]; if .[0] < 1 then break $out else [.[0] -1, $item] end; .[1]) end; def isempty(g): 0 == ((label $go | g | (1, break $go)) // 0); def first(g): label $out | g | ., break $out; def last(g): reduce g as $item (null; $item); def nth($n; g): if $n < 0 then error("nth doesn't support negative indices") else last(limit($n + 1; g)) end; def first: .[0]; def last: .[-1]; def nth($n): .[$n]; def combinations: if length == 0 then [] else .[0][] as $x | (.[1:] | combinations) as $y | [$x] + $y end; def combinations(n): . as $dot | [range(n) | $dot] | combinations; # transpose a possibly jagged matrix, quickly; # rows are padded with nulls so the result is always rectangular. def transpose: if . == [] then [] else . as $in | (map(length) | max) as $max | length as $length | reduce range(0; $max) as $j ([]; . + [reduce range(0;$length) as $i ([]; . + [ $in[$i][$j] ] )] ) end; def in(xs): . as $x | xs | has($x); def inside(xs): . as $x | xs | contains($x); def input: _input; def repeat(exp): def _repeat: exp, _repeat; _repeat; def inputs: try repeat(_input) catch if .=="break" then empty else .|error end; # like ruby's downcase - only characters A to Z are affected def ascii_downcase: explode | map( if 65 <= . and . <= 90 then . + 32 else . end) | implode; # like ruby's upcase - only characters a to z are affected def ascii_upcase: explode | map( if 97 <= . and . <= 122 then . - 32 else . end) | implode; # Streaming utilities def truncate_stream(stream): . as $n | null | stream | . as $input | if (.[0]|length) > $n then setpath([0];$input[0][$n:]) else empty end; def fromstream(i): foreach i as $item ( [null,false,null,false]; if ($item[0]|length) == 0 then [null,false,.[2],.[3]] elif ($item|length) == 1 and ($item[0]|length) < 2 then [null,false,.[0],.[1]] else . end | . as $state | if ($item|length) > 1 and ($item[0]|length) > 0 then [.[0]|setpath(($item|.[0]); ($item|.[1])), true, $state[2], $state[3]] else . end; if ($item[0]|length) == 1 and ($item|length == 1) and .[3] then .[2] else empty end, if ($item[0]|length) == 0 then $item[1] else empty end ); def tostream: {string:true,number:true,boolean:true,null:true} as $leaf_types | . as $dot | if $leaf_types[$dot|type] or length==0 then [[],$dot] else # We really need a _streaming_ form of `keys`. # We can use `range` for arrays, but not for objects. keys_unsorted as $keys | $keys[-1] as $last| ((# for each key $keys[] | . as $key | $dot[$key] | . as $dot | # recurse on each key/value tostream|.[0]|=[$key]+.), # then add the closing marker [[$last]]) end; # Assuming the input array is sorted, bsearch/1 returns # the index of the target if the target is in the input array; and otherwise # (-1 - ix), where ix is the insertion point that would leave the array sorted. # If the input is not sorted, bsearch will terminate but with irrelevant results. def bsearch(target): if length == 0 then -1 elif length == 1 then if target == .[0] then 0 elif target < .[0] then -1 else -2 end else . as $in # state variable: [start, end, answer] # where start and end are the upper and lower offsets to use. | [0, length-1, null] | until( .[0] > .[1] ; if .[2] != null then (.[1] = -1) # i.e. break else ( ( (.[1] + .[0]) / 2 ) | floor ) as $mid | $in[$mid] as $monkey | if $monkey == target then (.[2] = $mid) # success elif .[0] == .[1] then (.[1] = -1) # failure elif $monkey < target then (.[0] = ($mid + 1)) else (.[1] = ($mid - 1)) end end ) | if .[2] == null then # compute the insertion point if $in[ .[0] ] < target then (-2 -.[0]) else (-1 -.[0]) end else .[2] end end; # Apply f to composite entities recursively, and to atoms def walk(f): . as $in | if type == "object" then reduce keys_unsorted[] as $key ( {}; . + { ($key): ($in[$key] | walk(f)) } ) | f elif type == "array" then map( walk(f) ) | f else f end; # SQL-ish operators here: def INDEX(stream; idx_expr): reduce stream as $row ({}; .[$row|idx_expr| if type != "string" then tojson else . end] |= $row); def INDEX(idx_expr): INDEX(.[]; idx_expr); def JOIN($idx; idx_expr): [.[] | [., $idx[idx_expr]]]; def JOIN($idx; stream; idx_expr): stream | [., $idx[idx_expr]]; def JOIN($idx; stream; idx_expr; join_expr): stream | [., $idx[idx_expr]] | join_expr; def IN(s): reduce (first(select(. == s)) | true) as $v (false; if . or $v then true else false end); def IN(src; s): reduce (src|IN(s)) as $v (false; if . or $v then true else false end);