在Ruby中动态地对哈希数组进行排序

3

我想按照几个动态条件对哈希数组进行排序。假设我有以下数组:

persons = [
  {
    id: 1,
    first_name: "Bill",
    last_name: "Zamora",
    age: 37
  },
  {
    id: 2,
    first_name: "Alexia",
    last_name: "Reyes",
    age: 70
  },
  {
    id: 3,
    first_name: "Anthony",
    last_name: "Nelson",
    age: 25
  }
]

我知道您可以使用以下代码轻松地按多个条件对数组进行排序。
persons.sort_by!{ |p| [p[:age], p[:first_name]] }

然而,在这个例子中,用于排序数组的字段数量和顺序是硬编码的。在我的情况下,这是在运行时动态确定的。因此,我不知道数组要按多少字段排序,也不知道以哪种顺序对字段进行排序。
我正在寻找一种优雅的解决方案来使用我在之前不知道的配置对象对数组进行排序。这样的配置可能看起来像这样:
sort_settings = [
  {
    field: "first_name",
    order: "asc"
  },
  {
    field: "age",
    order: "desc"
  }
]

非常感谢您对此的任何帮助!


1
“desc” 部分可能并不像您想的那样简单。仅为实现该功能,代码就可能相当复杂。 - sawa
1
最好将sort_settings中的field值作为符号而不是字符串,因为您原始哈希表中的键是符号。 - sawa
对于排序,您可以始终按一个方向进行排序,然后使用#reverse来获取相反的顺序。更新:实际上,这只有在按多个具有不同顺序的字段进行排序时才有帮助。 - Kris
4个回答

3

使用sort_by对字符串进行降序排序是非常具有挑战性的,最好使用“低级别”的sort方法,该方法使用指定比较器使用<=>运算符进行排序。这个问题的快速解决方案如下:

persons.sort do |a, b|
  comparator = 0

  sort_settings.each do |s|
    a_field = a[s[:field].to_sym]
    b_field = b[s[:field].to_sym]

    comparator = a_field <=> b_field

    comparator = -comparator if s[:order] == "desc"

    break unless comparator == 0
  end

  comparator
end

该块必须实现a和b之间的比较,当a在b之后时返回-1,当a和b相等时返回0,或者如果b在a之后则返回+1。

因此,我们遍历sort_settings并使用<=>比较指定字段,它会返回10-1。如果指定的排序是desc,我们将反转该值。如果比较器返回非零值,则不需要继续迭代。


2

如果忽略 asc/desc 功能,假设排序键以符号形式给出,并且格式为:

sort_settings = [
  :first_name,
  :age,
]

您只需要做:

persons.sort_by{|p| p.values_at(sort_settings)}

1

代码

def sort_by_settings(persons, sort_settings)
  sort_mult_by_field = sort_settings.each_with_object({}) do |g,h|
    h[g[:field]] = g[:order] == "asc" ? 1 : -1
  end

  longest_string_by_key = persons.each_with_object(Hash.new(0)) do |g,h|
    g.each { |k,v| h[k] = [h[k], g[k].size].max if sort_mult_by_field.key?(k) &&
      v.is_a?(String) }
  end

  sort_by_arr = persons.each_with_object({}) do |g,h|    
    h[g] = sort_mult_by_field.each_with_object([]) do |(f,m),a|
      gv = g[f]
      a <<
      case gv
      when Integer
        m * gv
      when String
        gv.chars.map { |c| m * c.ord }.concat([m * -256]*(longest_string_by_key[f]-gv.size))
      else # rescue...
      end
    end
  end

  persons.sort_by { |g| sort_by_arr[g] }
end

例子

persons 按照问题中的定义。

sort_settings = [{field: :first_name, order: "asc"}, {field: :age, order: "desc"}]

sort_by_settings(persons, sort_settings)
  #=> [{:id=>2, :first_name=>"Alexia",  :last_name=>"Reyes",  :age=>70},
  #    {:id=>3, :first_name=>"Anthony", :last_name=>"Nelson", :age=>25},
  #    {:id=>1, :first_name=>"Bill",    :last_name=>"Zamora", :age=>37}]

persons1 = persons + [{ id: 4, first_name: "Alexia", last_name: "Whoosit", age: 71 }]
sort_by_settings(persons1, sort_settings)
  #=> [{:id=>4, :first_name=>"Alexia",  :last_name=>"Whoosit", :age=>71},
  #    {:id=>2, :first_name=>"Alexia",  :last_name=>"Reyes",   :age=>70},
  #    {:id=>3, :first_name=>"Anthony", :last_name=>"Nelson",  :age=>25},
  #    {:id=>1, :first_name=>"Bill",    :last_name=>"Zamora",  :age=>37}]

sort_settings1 = [{field: :first_name, order: "desc"}, {field: :age, order: "asc"}]
sort_by_settings(persons1, sort_settings1)
  #=> [{:id=>1, :first_name=>"Bill",    :last_name=>"Zamora",  :age=>37},
  #    {:id=>3, :first_name=>"Anthony", :last_name=>"Nelson",  :age=>25},
  #    {:id=>2, :first_name=>"Alexia",  :last_name=>"Reyes",   :age=>70}, 
  #    {:id=>4, :first_name=>"Alexia",  :last_name=>"Whoosit", :age=>71}]

解释

在第一个示例的计算中,计算了以下中间值。

sort_mult_by_field
  #=> {:first_name=>1, :age=>-1}

longest_string_by_key
  #=> {:first_name=>7}

sort_by_arr
  #=> {{:id=>1, :first_name=>"Bill",    :last_name=>"Zamora", :age=>37}=>
  #      [[66, 105, 108, 108, -256, -256, -256], -37],
  #    {:id=>2, :first_name=>"Alexia",  :last_name=>"Reyes",  :age=>70}=>
  #      [[65, 108, 101, 120,  105,   97, -256], -70],
  #    {:id=>3, :first_name=>"Anthony", :last_name=>"Nelson", :age=>25}=>
  #      [[65, 110, 116, 104,  111,  110,  121], -25]}

做得好!唯一能正常工作的是“asc”和“desc”。 - iGian

0
你需要一个方法,根据配置指南将给定的项目转换为排序键:
def build_sort_key_for(item, configuration)
  configuration.map { |entry|
    value = item[entry[:field].to_sym]
    value = -value if entry[:order] == "desc" # this will only work on numeric values
    value
  }
end

然后你只需要在你的 sort_by 中调用它即可:
persons.sort_by!{ |p| build_sort_key_for(p, configuration) }

使用"desc"让字符串正常工作本身就是一项挑战,因此需要读者(或提出另一个问题)来解决。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接