【问题标题】:sorting and rearranging an array of hashes based on multiple conditions根据多个条件对哈希数组进行排序和重新排列
【发布时间】:2019-02-12 06:25:48
【问题描述】:

我正在尝试根据 3 个不同的标准对数组进行排序。假设我有一个这样的哈希数组:

a = [
    { "name" => "X", "year" => "2013-08"},
    { "name" => "A", "year" => "2017-01"},
    { "name" => "X", "year" => "2000-08"},
    { "name" => "B", "year" => "2018-05"},
    { "name" => "D", "year" => "2016-04"},
    { "name" => "C", "year" => "2016-04"}
]

我想先按“年份”降序排序所有元素,然后按“名称”升序排序,然后将所有匹配给定名称的元素移动到数组的开头,同时仍然尊重“年份”命令。对于此示例,假设我正在寻找“名称”值为“X”的元素。所以我正在寻找的输出是:

{"name"=>"X", "year"=>"2013-08"}
{"name"=>"X", "year"=>"2000-08"}
{"name"=>"B", "year"=>"2018-05"}
{"name"=>"A", "year"=>"2017-01"}
{"name"=>"C", "year"=>"2016-04"}
{"name"=>"D", "year"=>"2016-04"}

所以一切都是按“年份”的降序排列,然后是“名称”的升序,然后是“名称”==“X”的所有哈希移到顶部,仍然按“年份”排序。

我通过这样做来处理升序/降序排序:

a.sort { |a,b| [b["year"], a["name"]] <=> [a["year"], b["name"]] }        

但这仅处理我需要的前两个标准。后来我尝试了这样的事情:

top = []
a.each { |x| top << x if x["name"] == "X" }
a.delete_if { |x| x["name"] == "X"}
a.unshift(top)

它确实产生了所需的输出,但显然很笨重,似乎不是最好的做事方式。有没有更快、更有效的方法来做我想做的事情?

(仅供参考,年份值是字符串,我无法将它们转换为整数。我在这里简化了值,但我从中提取的数据实际上在每个值的末尾附加了一系列其他字符和符号.)

【问题讨论】:

    标签: arrays ruby sorting hash


    【解决方案1】:

    sort 不是你想要使用的东西,如果你有一致的排序标准。更快的方法是sort_by

    a.sort_by { |e| [ e["year"], e["name"] ] }
    

    因为您希望它们以相反的顺序排列:

    a.sort_by { |e| [ e["year"], e["name"] ] }.reverse
    

    实际上是根据块中表示的转换形式对数组中的每个元素进行排序,然后根据这些形式进行排序。这种转换只进行一次,这比sort 方法要麻烦得多,sort 方法每次进行比较时都必须执行该转换。

    现在,如果您想将“X”条目排序到顶部,您可以轻松地将其添加为附加条件:

    a.sort_by { |e| [ e["name"] == "X" ? 1 : 0, e["year"], e["name"] ] }.reverse
    

    这样你就可以到达你想去的地方。

    sort_by 的好处是您通常可以将非常复杂的排序逻辑表示为数组中的一系列元素。如果每个元素都是 Comparable ,那么一切都会成功。

    【讨论】:

    • 非常感谢!我实际上曾尝试使用sort_byreverse,但问题是我只希望反转“年份”,而不是“名称”(即降序年份,但升序名称)。因此,如果两个元素具有相同的“年份”,它们将按字母顺序列出。有没有办法将reverse 专门应用于e["year"] 部分?
    • 对于e["name"] != "X",将通过减少e["year"] 的值来完成排序,而通过增加 e["name"] 的值来打破平局。您已经对两者的递减值进行了排序,因此排序后的数组以 [... {"name"=&gt;"D", "year"=&gt;"2016-04"}, {"name"=&gt;"C", "year"=&gt;"2016-04"}] 结束,这与所需的顺序相反。
    • 由于您的“年份”主要是数字,您可以尝试将-e["year"].sub(/\-/, '').to_i 作为排序键,将其取反为倒序。
    【解决方案2】:

    这是另一种选择,使用你已经拥有的东西作为基础(因为你基本上一直在那里)

    a = [
      { "name" => "X", "year" => "2013-08"},
      { "name" => "A", "year" => "2017-01"},
      { "name" => "X", "year" => "2000-08"},
      { "name" => "B", "year" => "2018-05"},
      { "name" => "D", "year" => "2016-04"},
      { "name" => "C", "year" => "2016-04"}
    ]
    
    
    a.sort do  |a,b| 
      a_ord, b_ord = [a,b].map {|e| e["name"] == "X" ? 0 : 1 }
      [a_ord,b["year"],a["name"] ] <=> [b_ord, a["year"],b["name"]]
    end
    

    在这里,我们只是确保“X”总是在前面,方法是为它分配一个 0,其他所有东西都分配一个 1。然后,因为 0 和 0 是等价的,所以 X 将回退到你已经应用的相同逻辑,就像所有其他。我们可以让它更漂亮一点:

    a.sort do  |a,b| 
      [a,b].map {|e| e["name"] == "X" ? 0 : 1 }.zip(
        [b["year"],a["year"]],[a["name"],b["name"]]
      ).reduce(:<=>)
    end
    

    【讨论】:

      【解决方案3】:
      arr = [
        {"name"=>"X", "year"=>"2013-08"},
        {"name"=>"X", "year"=>"2000-08"},
        {"name"=>"B", "year"=>"2018-05"},
        {"name"=>"A", "year"=>"2017-01"},
        {"name"=>"C", "year"=>"2016-04"},
        {"name"=>"D", "year"=>"2016-04"},
      ]
      

      当数组的某些部分的排序方式与数组的其他部分不同时,我发现将数组划分为相关的部分,分别对每个部分进行排序,然后组合这些排序的结果是有益的。这种方法不仅通常易于读者理解,而且还简化了测试,并且往往至少与执行单个更复杂的排序一样有效。在这里,我们将数组分成两部分。

      x, non_x = arr.partition { |h| h["name"] == 'X' }
        #=> [[{"name"=>"X", "year"=>"2013-08"}, {"name"=>"X", "year"=>"2000-08"}],
        #    [{"name"=>"B", "year"=>"2018-05"}, {"name"=>"A", "year"=>"2017-01"},
        #     {"name"=>"C", "year"=>"2016-04"}, {"name"=>"D", "year"=>"2016-04"}]]
      

      对数组x 进行排序很容易。

      sorted_x = x.sort_by { |h| h["year"] }.reverse
        #=> [{"name"=>"X", "year"=>"2013-08"}, {"name"=>"X", "year"=>"2000-08"}]
      

      non_x 进行排序比较复杂,因为它是按"year" 的值的递减 顺序排序的,并由"name" 的值打破,在递增顺序。在这种情况下,我们总是可以使用Array#sort

      non_x.sort do |g,h|
        case g["year"] <=> h["year"]
        when -1
          1
        when 1
          -1
        when 0
          (g["name"] < h["name"]) ? -1 : 1
        end
      end
        #=> [{"name"=>"B", "year"=>"2018-05"}, {"name"=>"A", "year"=>"2017-01"},
        #    {"name"=>"C", "year"=>"2016-04"}, {"name"=>"D", "year"=>"2016-04"}]
      

      稍加努力,我们也可以使用Enumerable#sort_by。给定一个哈希h,我们需要对其中一个进行排序

      [h["year"], f(h["name"])].reverse
      

      其中f 是一种使h["name"] 按降序排序的方法,或者(请注意下面没有.reverse

      [f(h["year"]), h["name"]]
      

      其中f 是一种使h["year"] 按降序排序的方法。后者是两者中更容易实现的。我们可以使用下面的方法。

      def year_str_to_int(year_str)
        yr, mon = year_str.split('-').map(&:to_i)
        12 * yr + mon
      end
      

      这允许我们根据需要对non_x 进行排序:

      sorted_non_x = non_x.sort_by { |h| [-year_str_to_int(h["year"]), h["name"]] }
        #=> [{"name"=>"B", "year"=>"2018-05"}, {"name"=>"A", "year"=>"2017-01"},
        #    {"name"=>"C", "year"=>"2016-04"}, {"name"=>"D", "year"=>"2016-04"}]
      

      我们现在简单地组合两个排序的分区。

      sorted_x.concat(sorted_non_x)
        #=> [{"name"=>"X", "year"=>"2013-08"}, {"name"=>"X", "year"=>"2000-08"},
        #    {"name"=>"B", "year"=>"2018-05"}, {"name"=>"A", "year"=>"2017-01"}, 
        #    {"name"=>"C", "year"=>"2016-04"}, {"name"=>"D", "year"=>"2016-04"}]
      

      【讨论】:

        【解决方案4】:

        您可以通过为您的对象实现可比较逻辑来编写自己的比较器,如下所示:

        require 'pp'
        
        a = [
            { "name" => "X", "year" => "2013-08"},
            { "name" => "A", "year" => "2017-01"},
            { "name" => "X", "year" => "2000-08"},
            { "name" => "B", "year" => "2018-05"},
            { "name" => "D", "year" => "2016-04"},
            { "name" => "C", "year" => "2016-04"}
        ]
        
        class NameYearSorter
          attr_reader :value
          def initialize(value)
            @value = value
          end
        
          def name
            value['name']
          end
        
          def year
            value['year']
          end
        
          def <=>(other)
            if self.name != 'X' && other.name != 'X'
              if self.year == other.year
                self.name <=> other.name
              else
                self.year > other.year ? -1 : 0
              end
            elsif self.name == 'X' && other.name != 'X'
              -1
            elsif other.name == 'X' && self.name != 'X'
              0   
            elsif self.name == other.name
              other.year > self.year ? 0 : -1
            end
          end
        end
        
        sortable = a.map{ |v| NameYearSorter.new(v) }
        pp sortable.sort.map(&:value)
        
        # Output:
        #=> [{"name"=>"X", "year"=>"2013-08"},
        #=>  {"name"=>"X", "year"=>"2000-08"},
        #=>  {"name"=>"B", "year"=>"2018-05"},
        #=>  {"name"=>"A", "year"=>"2017-01"},
        #=>  {"name"=>"C", "year"=>"2016-04"},
        #=>  {"name"=>"D", "year"=>"2016-04"}]
        

        【讨论】:

          猜你喜欢
          • 2015-04-08
          • 2019-06-23
          • 1970-01-01
          • 2012-02-11
          • 2019-04-25
          • 2012-03-07
          • 1970-01-01
          • 1970-01-01
          • 2013-09-30
          相关资源
          最近更新 更多