获取jq中的对象数组索引

时间:2017-01-31 13:09:56

标签: arrays json indexof jq

我有一个看起来像这样的json对象(由i3-msg -t get_workspaces生成。

[
  {
    "name": "1",
    "urgent": false
  },
  {
    "name": "2",
    "urgent": false
  },
  {
    "name": "something",
    "urgent": false
  }
]

我正在尝试使用jq来确定列表中的哪个索引号基于select查询。 jq有一个叫index()的东西,但它只能支持字符串吗?

使用像i3-msg -t get_workspaces | jq '.[] | select(.name=="something")'这样的东西给了我想要的对象。但我想要它的索引。在这种情况下2(从0开始计数)

这是否可以单独使用jq

4 个答案:

答案 0 :(得分:6)

所以我提供了一个解决OP的策略,OP很快就接受了。随后@peak和@Jeff Mercado提供了更好,更完整的解决方案。所以我把它变成了社区维基。如果可以的话,请改进这个答案。

一个简单的解决方案(由@peak指出)是使用内置函数index

map(.name == "something") | index(true)

jq文档令人困惑地建议index对字符串进行操作,但它也在数组上运行。因此index(true)返回地图生成的布尔数组中第一个true的索引。如果没有满足条件的项,则结果为空。

jq表达式在"lazy" manner中计算,但map将遍历整个输入数组。我们可以通过重写上面的代码并引入一些调试语句来验证这一点:

[ .[] | debug | .name == "something" ] | index(true)

正如@peak所建议的,做得更好的关键是使用jq 1.5中引入的break语句:

label $out | 
foreach .[] as $item (
  -1; 
  .+1; 
  if $item.name == "something" then 
    ., 
    break $out 
  else 
    empty
  end
) // null

请注意,//不予置评;它是替代运营商。如果找不到该名称,则foreach将返回empty,其将由替代运算符转换为null。

另一种方法是递归处理数组:

def get_index(name): 
  name as $name | 
  if (. == []) then
    null
  elif (.[0].name == $name) then 
    0 
  else 
    (.[1:] | get_index($name)) as $result |
    if ($result == null) then null else $result+1 end      
end;
get_index("something")

然而,正如@Jeff Mercado所指出的,在最坏的情况下,这种递归实现将使用与数组长度成比例的堆栈空间。在版本1.5 jq中引入了Tail Call Optimization (TCO),这将允许我们使用本地帮助函数来优化它(请注意,这是对@Jeff Mercado提供的解决方案的小修改,以便与上述一致实施例):

def get_index(name): 
  name as $name | 
  def _get_index:
    if (.i >= .len) then
      null
    elif (.array[.i].name == $name) then
      .i
    else
      .i += 1 | _get_index
    end;
  { array: ., i: 0, len: length } | _get_index;
get_index("something")

根据@peak获得jq中数组的长度是一个恒定时间操作,显然索引数组也很便宜。我会尝试为此找到一个引用。

现在让我们尝试实际测量。以下是衡量简单解决方案的示例:

#!/bin/bash

jq -n ' 

  def get_index(name): 
    name as $name |
    map(.name == $name) | index(true)
  ;

  def gen_input(n):  
    n as $n |
    if ($n == 0) then 
      []
    else
      gen_input($n-1) + [ { "name": $n, "urgent":false } ]
    end
  ;  

  2000 as $n |
  gen_input($n) as $i |
  [(0 | while (.<$n; [ ($i | get_index(.)), .+1 ][1]))][$n-1]
'

当我在我的机器上运行时,我得到以下内容:

$ time ./simple
1999

real    0m10.024s
user    0m10.023s
sys     0m0.008s

如果我用&#34; fast&#34; get_index的版本:

def get_index(name): 
  name as $name |
  label $out | 
  foreach .[] as $item (
    -1; 
    .+1; 
  if $item.name == $name then 
    ., 
    break $out 
  else 
    empty
  end
) // null;

然后我得到:

$ time ./fast
1999

real    0m13.165s
user    0m13.173s
sys     0m0.000s

如果我用&#34; fast&#34;递归版:

def get_index(name): 
  name as $name | 
  def _get_index:
    if (.i >= .len) then
      null
    elif (.array[.i].name == $name) then
      .i
    else
      .i += 1 | _get_index
    end;
  { array: ., i: 0, len: length } | _get_index;

我明白了:

$ time ./fast-recursive 
1999

real    0m52.628s
user    0m52.657s
sys     0m0.005s

哎哟!但我们可以做得更好。 @peak提到了一个未记录的开关--debug-dump-disasm,它可以让您了解jq如何编译代码。有了这个,您可以看到修改对象并将其传递给_indexof,然后提取数组,长度和索引是很昂贵的。重构只是传递索引是一个巨大的改进,并且进一步改进以避免测试索引的长度使其与迭代版本竞争:

def indexof($name):
  (.+[{name: $name}]) as $a | # add a "sentinel"
  length as $l | # note length sees original array
  def _indexof:
    if ($a[.].name == $name) then
      if (. != $l) then . else null end
    else
      .+1 | _indexof
    end
  ;


  0 | _indexof
;

我明白了:

$ time ./fast-recursive2
null

real    0m13.238s
user    0m13.243s
sys     0m0.005s

因此,如果每个元素都具有相同的可能性,并且您希望获得平均案例性能,那么您应该坚持使用简单的实现。 (C编码函数往往很快!)

答案 1 :(得分:4)

@ Jim-D最初使用foreach提出的解决方案只能用于JSON对象的数组,并且最初提出的解决方案效率都非常低。他们在缺乏满足条件的项目时的行为也可能令人惊讶。

使用index/1

的解决方案

如果您只想要一个快速简便的解决方案,可以使用内置函数index,如下所示:

map(.name == "something") | index(true)

如果没有项目满足条件,则结果为null

顺便提一下,如果您想要条件为真的所有索引,那么只需将index更改为indices即可轻松将上述转换为超快解决方案:

map(.name == "something") | indices(true)

高效解决方案

这是一个通用且高效的函数,它返回输入数组中第一次出现的项的索引(即偏移量),其中(item | f)是真的(既不是null也不是false),{{1} } 除此以外。 (在jq,javascript和许多其他方面,数组的索引总是从0开始。)

null

使用示例:

# 0-based index of item in input array such that f is truthy, else null
def which(f):
  label $out
  | foreach .[] as $x (-1; .+1; if ($x|f) then ., break $out else empty end)
  // null ;

答案 2 :(得分:2)

将数组转换为条目将使您可以访问项目数组中的索引和值。您可以使用它来找到您要查找的值并获取其索引。

    select * from table1 t1
left join table2 t2 on t1.b1 = t2.bserial
left join table2 t3 on t1.m1 = t3.mserial
where t1.sserial = 'some value'

然而,这不会短路并且将遍历整个阵列以找到索引。一旦找到第一个索引,您就会想要返回。采用更具功能性的方法可能更合适。

def indexof(predicate):
    reduce to_entries[] as $i (null;
        if (. == null) and ($i.value | predicate) then
            $i.key
        else
            .
        end
    );
indexof(.name == "something")

请注意,参数以这种方式传递给内部函数以利用some optimizations。即,为了利用TCO,该功能不得接受任何其他参数。

通过识别数组及其长度不变,可以获得更快的版本:

def indexof(predicate):
    def _indexof:
        if .i >= .len then
            null
        elif (.arr[.i] | predicate) then
            .i
        else
            .i += 1 | _indexof
        end;
    { arr: ., i: 0, len: length } | _indexof;
indexof(.name == "something")

答案 3 :(得分:0)

这是另一个版本,它似乎比@peak 和 @jeff-mercado 的优化版本稍快:

label $out | . as $elements | range(length) |
select($elements[.].name == "something") | . , break $out

IMO 虽然它仍然依赖于 break(仅获取第一个匹配项),但它更易于阅读。

我在大约 1,000,000 个元素数组上进行了 100 次迭代(最后一个元素是要匹配的元素)。我只计算了用户和内核时间,而不是挂钟时间。这个解决方案平均需要 3.4 秒,@peak 的解决方案需要 3.5 秒,@jeff-mercado 的需要 3.6 秒。这与我在一次运行中看到的相符,尽管公平地说,我确实有一次运行,该解决方案平均为 3.6 秒,因此每个解决方案之间不太可能有任何统计上的显着差异。