根据多种条件对哈希数组进行排序和重新排列

时间:2018-09-06 23:38:46

标签: arrays ruby sorting hash

我正在尝试根据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)

确实会产生所需的输出,但是显然很笨重,而且似乎不是执行操作的最佳方法。有没有更快,更有效的方式来做我想做的事情?

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

4 个答案:

答案 0 :(得分:3)

如果您具有一致的排序条件,则不希望使用

sort。更快的方法是sort_by

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

因为您希望它们以相反的顺序出现:

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

实际上是根据块中表达的转换形式对数组中的每个元素进行排序,然后根据这些元素进行排序。此转换只执行一次,并且比sort方法要麻烦得多,a.sort_by { |e| [ e["name"] == "X" ? 1 : 0, e["year"], e["name"] ] }.reverse 方法每次进行比较时都必须执行该转换。

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

sort_by

这样可以将您带到想要的地方。

Activity的好处是,您通常可以将非常复杂的排序逻辑表示为数组中的一系列元素。只要每个元素都是可比较的,就可以解决所有问题。

答案 1 :(得分:1)

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"的值的降序排序,并用{{ 1}},按递增顺序。在这种情况下,我们始终可以使用Array#sort

"name"

稍作努力,我们可以选择使用Enumerable#sort_by。给定一个哈希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"}] ,我们将需要对两者进行排序

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"}]

答案 2 :(得分:1)

您可以通过为对象实现类似的逻辑来编写自己的比较器:

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"}]

答案 3 :(得分:1)

这是另一个使用您已经拥有的基础的选项(因为您基本上一直都在那儿)

validation_accuracy

在这里,我们只需为“ X”分配一个0并将其他所有值都分配给1,以确保始终在前面。然后,由于0和0等效,因此X会退回到您已经应用的相同逻辑其他。我们可以这样设置:

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