【问题标题】:Sort array of hashes based on value of key in hash?根据散列中键的值对散列数组进行排序?
【发布时间】:2015-02-13 19:00:42
【问题描述】:

我正在尝试与 Vagrant 合作,在启动 Docker 容器时执行一些自动化操作。 Vagrantfiles 本质上是 Ruby,因此我应该能够应用 Ruby 逻辑来帮助解决这个问题。

我正在阅读一个 conf.d 目录,该目录中充满了包含配置数据的 YAML 文件,然后将配置项的哈希值推送到一个数组中。完成后,我将使用 .each 遍历数组,并根据散列中某些键的值将配置应用于数组中的每个条目。这些键之一是“链接”。链接的值将与另一个键“名称”的值相关。

我基本上需要确保link => 'name' 的哈希值在name => 'value' 的哈希值之前的数组中。

输入和预期输出示例:

输入

containers = [{"name"=>"foo", "ports"=>["80:80", "443:443"], "links"=>["bar", "baz"]}, {"name"=>"bar", "ports"=>["8888:8888"]}, {"name"=>"baz","ports"=>"80:80"}]

预期输出

containers = [{"name"=>"bar", "ports"=>["8888:8888"]}, {"name"=>"baz", "ports"=>"80:80"}, {"name"=>"foo", "ports"=>["80:80", "443:443"], "links"=>["bar", "baz"]}]

最终结果是,任何带有“链接”的条目都会出现在数组中哈希的名称键与之匹配的条目之后。 (基本上是基于链接键的依赖排序。)

请注意,链接的容器可能会链接到另一个链接的容器。

这让我有点困惑,因为我知道我需要做什么,但缺乏真正弄清楚“如何做”的技术能力。 :)

提前感谢您的帮助。

【问题讨论】:

  • 感谢@john-ledbetter 的编辑。忘记在此处附上正确的格式。
  • 如果你只提供示例输入和预期输出会更清楚
  • 编辑了问题以阐明预期的输入/输出。对不起,谢谢。
  • 我建议您编辑以删除对 Vagrant、Docker 和 YAML 的引用。他们只是混淆了这个问题,(据我所知)是如何对哈希数组进行排序以实现所需的排序,即g 遵循h 如果h 有一个键"links"h["links"]包括g["name"]

标签: ruby vagrant


【解决方案1】:

这应该适合你:

def order_containers(containers)
  unordered = containers.dup
  ordered = []
  names_from_ordered = {}
  name_is_ordered = names_from_ordered.method(:[])
  until unordered.empty?
    container = unordered.find do |c|
      c.fetch('links', []).all? &name_is_ordered
    end
    raise 'container ordering impossible' if !container
    ordered << container
    unordered.delete(container)
    names_from_ordered[container.fetch('name')] = true
  end
  ordered
end

containers = [
  { 'name'=>'foo', 'links'=>['bar'] },
  { 'name'=>'a', 'links'=>['goo'] },
  { 'name'=>'bar' },
  { 'name'=>'goo', 'links'=>['foo'] },
]

containers = order_containers(containers)

require 'pp'
pp containers
# => [{"name"=>"bar"},
#     {"name"=>"foo", "links"=>["bar"]},
#     {"name"=>"goo", "links"=>["foo"]},
#     {"name"=>"a", "links"=>["goo"]}]

基本思想是我们使用一个循环,循环的每一次迭代都会从输入列表中找到一个适合添加到输出列表的容器。如果容器所依赖的所有容器都已添加到输出列表中,则该容器适合添加到输出列表中。然后将容器从输入列表中删除并添加到输出列表中。

这个循环可以通过两种主要方式终止:

  1. 当输入列表为空时,表示成功,或者
  2. 当我们找不到可以启动的容器时,这将是循环依赖导致的错误。

【讨论】:

    【解决方案2】:

    在我看来,最简单的事情是这样的:

    linkless_configs = []
    linked_configs = []
    if config_hash.has_key?("links")
      linked_configs.push(config_hash)
    else
      linkless_configs.push(config_hash)
    end
    

    然后您可以迭代 linkless_configs + linked_configs 并保证每个链接的配置都在相应的无链接配置之后。

    或者,如果你必须排序,你可以

    containers.sort_by { |config| config.has_key?("links") ? 1 : 0 }
    

    【讨论】:

    • 这是一个很棒的想法,不幸的是我确实需要考虑一个链接容器可能链接到另一个链接容器的事实。我应该在上述问题中澄清这一点。对不起。谢谢。
    【解决方案3】:

    [编辑: @DavidGrayson 指出我的回答存在缺陷。我会看看我是否能找到解决办法,但如果我不能,我担心可能是这种情况,我会删除答案。 [编辑#2:哦,天哪!在我最初的编辑之后,有人赞成我的回答。我不确定我现在可以删除它,但说实话,我已经决定不这样做,主要是因为我的解释对任何针对 OP 问题的建议解决方案都有影响。在余额中有 10 分的情况下,将其保留现在更加引人注目。 2#tidE]

    我相信我理解这个问题。 sort 需要一个全序,这是一个偏序,其中每对元素都有a &lt;= ba &lt;= bref 后者没问题,但偏序要求是。偏序必须满足以下公理:

    • 自反性 (x ≤ x),
    • 反对称(如果x ≤ yy ≤ x 然后x = y)和
    • 传递性(如果x ≤ yy ≤ z,那么x ≤ z)。

    我的排序只满足反身公理。大卫给出了反例:

    containers = [h0, h1, h2]
    

    在哪里

    h0 = {'name'=>'foo', 'links'=>['bar']},
    h1 = {'name'=>'a'},
    h2 = {'name'=>'bar'},
    
    containers.sort
      #=> [{"name"=>"foo", "links"=>["bar"]},
      #    {"name"=>"a"}, {"name"=>"bar"}]
    

    我的方法Hash#&lt;=&gt;建立:

    h0 = h1
    h0 > h2
    h1 = h2
    

    如果sort 找到h0 = h1 = h2,它将通过传递性得出h0 = h2(而不检查h0 &lt;=&gt; h2),这可能会导致错误的结果。

    David 还指出 o.follows?(self) 应该引发异常,因为我已将其定义为 private。由于我还没有遇到异常,所以我断定该语句没有被执行,但我没有追查其原因,但这是一个小问题(尽管无疑是一个有用的线索)。

    感谢大卫发现问题。当然,不正确的答案需要公开,但我觉得我也学到了一些有用的东西。

    潮]

    如果我正确理解了问题,并且数据提供了有效的排序,我认为您可以按以下方式进行。

    class Hash
      def <=>(o)
        case
        when   follows?(o)    then  1
        when o.follows?(self) then -1
        else                        0
        end
      end
    
      private
    
      def follows?(o)
        key?("links") && self["links"].include?(o["name"])
      end
    end
    
    containers = [{"name"=>"foo", "ports"=>["80:80", "443:443"],
                   "links"=>["bar", "baz"]},
                  {"name"=>"bar", "ports"=>["8888:8888"]},
                  {"name"=>"baz","ports"=>"80:80"}]
    
    containers.sort
      #=> [{"name"=>"baz", "ports"=>"80:80"},
      #    {"name"=>"bar", "ports"=>["8888:8888"]},
      #    {"name"=>"foo", "ports"=>["80:80", "443:443"],
      #     "links"=>["bar", "baz"]}] 
    

    附录

    尽管我假设数据提供了有效的排序,但@Ajedi32 询问当存在循环引用时会发生什么。让我们找出答案:

    containers = [{"name"=>"foo", "links"=>["bar"]},
                  {"name"=>"bar", "links"=>["baz"]},
                  {"name"=>"baz", "links"=>["foo"]}]
    containers.sort
      #=> [{ "name"=>"baz", "links"=>["foo"] },
      #    { "name"=>"bar", "links"=>["baz"] },
      #    { "name"=>"foo", "links"=>["bar"] }]
    
    containers = [{"name"=>"foo", "links"=>["bar"]},
                  {"name"=>"bar", "links"=>["foo"]}]
    containers.sort
      #=> [{ "name"=>"bar", "links"=>["foo"] },
      #    { "name"=>"foo", "links"=>["bar"] }]
    

    这表明,如果不确定没有循环引用,则应在排序前检查。

    【讨论】:

    • 请不要使用特定于域的逻辑对核心类进行修补,以便您可以对数组进行排序。
    • @Ajedi32,将&lt;=&gt; 添加到Hash 还不错,因为Hash 没有实现该方法。 (它确实从Module 继承了该方法,但在比较哈希时它总是返回nil。)我这样做是为了简化解释,但我同意你的观点,弄乱核心类不是一个好习惯.我相信您知道,我们可以为数组的元素创建一个类并为该类定义 &lt;=&gt;,或者在 Ruby 2.1+ 中使用 Refinements
    • Cary,由于follows? 是私有的,此时您应该会收到错误消息:o.follows?(self)
    • 您的飞船操作员与普通的飞船操作员有很大不同,所以我认为排序算法不能保证给出正确的结果。这是一个给出错误结果的示例:gist.github.com/DavidEGrayson/3ece486a516e142a113d
    • @Ajedi32,您可能对 David 的 cmets 和我的编辑感兴趣。
    猜你喜欢
    • 2015-01-26
    • 2014-11-07
    • 2014-01-08
    • 1970-01-01
    • 2017-07-30
    • 2015-02-03
    • 2010-11-02
    • 1970-01-01
    • 2014-05-07
    相关资源
    最近更新 更多