Ruby按多个字段和不同数据类型的多个方向排序

时间:2015-01-30 11:20:45

标签: ruby

我需要编写一个multi_sort方法来对接受哈希作为参数的哈希数组进行排序,例如:{a_sort:1,display_sort:1}。此哈希是排序字段和排序方向。 (1表示升序,-1表示降序)。

items = [ {name: 'Album 1', a_sort: 5, display_sort: 3}, 
          {name: 'Album 2', a_sort: 1, display_sort: 5}, 
          {name: 'Album 3', a_sort: 3, display_sort: 2},
          {name: 'Album 5', a_sort: 1, display_sort: 8}, 
          {name: 'Album 7', a_sort: 5, display_sort: 1}, 
          {name: 'Album 7', a_sort: 5, display_sort: 6} ]

multi_sort(items, {a_sort: 1, display_sort: 1})

3小时后我无法弄明白。预期的输出是正确排序的数组。

      [ {name: 'Album 2', a_sort: 1, display_sort: 5},
        {name: 'Album 5', a_sort: 1, display_sort: 8},
        {name: 'Album 3', a_sort: 3, display_sort: 2},
        {name: 'Album 7', a_sort: 5, display_sort: 1}
        {name: 'Album 1', a_sort: 5, display_sort: 3}, 
        {name: 'Album 7', a_sort: 5, display_sort: 6} ]

3 个答案:

答案 0 :(得分:4)

这仅适用于散列中的数值:

sort_by除了要排序的项目,但由于我们要按许多项目排序,我们应该创建一个列表。

由于我们希望能够确定方向,因此我们将此列表中的每个值映射为自身( 1)或负自我( -1),从而导致排序相反。

items = [ {name: 'Album 1', a_sort: 5, display_sort: 3}, 
          {name: 'Album 2', a_sort: 1, display_sort: 5}, 
          {name: 'Album 3', a_sort: 3, display_sort: 2},
          {name: 'Album 5', a_sort: 1, display_sort: 8}, 
          {name: 'Album 7', a_sort: 5, display_sort: 1}, 
          {name: 'Album 7', a_sort: 5, display_sort: 6} ]

def multi_sort(items, sort_directions)
  items.sort_by do |row|
    sort_directions.map{|key,val| val*row[key]} 
  end
end

puts multi_sort(items, {a_sort: 1, display_sort: -1}) # =>

#{:name=>"Album 5", :a_sort=>1, :display_sort=>8}
#{:name=>"Album 2", :a_sort=>1, :display_sort=>5}
#{:name=>"Album 3", :a_sort=>3, :display_sort=>2}
#{:name=>"Album 7", :a_sort=>5, :display_sort=>6}
#{:name=>"Album 1", :a_sort=>5, :display_sort=>3}
#{:name=>"Album 7", :a_sort=>5, :display_sort=>1}

这是一个使用sort的版本,有点复杂,可以对<=>运算符可以排序的任何数据进行排序:

def multi_sort(items, sort_directions)
  items.sort do |row1,row2|
    keys = sort_directions.map{|key,val| val*(row1[key]<=>row2[key])}
    keys.find{|x|x!=0} || 0
  end
end

puts multi_sort(items, {a_sort: -1, display_sort: 1}) # =>

#{:name=>"Album 1", :a_sort=>5, :display_sort=>"a"}
#{:name=>"Album 7", :a_sort=>5, :display_sort=>"a"}
#{:name=>"Album 7", :a_sort=>5, :display_sort=>"b"}
#{:name=>"Album 3", :a_sort=>3, :display_sort=>"b"}
#{:name=>"Album 5", :a_sort=>1, :display_sort=>"b"}
#{:name=>"Album 2", :a_sort=>1, :display_sort=>"v"}

以下是它的工作原理。 Sort有两个参数(row1,row2),两个应该比较的项目,并期望返回值为-1,0或1.当arg1&gt; arg2然后是-1,当arg2 == arg1然后是0,当arg1

所以,我们需要做的就是解决这个问题。我拿了他们的密钥,对于散列中的每个值,我使用<=>函数应用map。我将结果乘以1或-1以得到相反的效果。然后,我刚刚通过比较列表并选择第一个非零值。如果所有列都相同,则数组将以零填满,并且排序将调用它们相等。

答案 1 :(得分:4)

非常有趣的问题。我也认为sort_by方法最有帮助。 我的解决方案(仅限数值)的工作方式如下:

DIRECTION_MULTIPLIER = { asc: 1, desc: -1 }

def multi_sort(items, order)
  items.sort_by do |item|
    order.collect do |key, direction|
      item[key]*DIRECTION_MULTIPLIER[direction]
    end
  end
end

# ... items ...
multi_sort(items, a_sort: :asc, display_sort: :desc)

我们的想法是为sort_by传递的每个项目构建一个列表。此列表由给出排序顺序的所有值组成。因此,我们使用Ruby知道[1,2]小于[1,3]但大于[0,0]

需要注意的一个有趣的部分是函数的最后一个参数将作为一个哈希传递,并且这些哈希对的顺序将保持。 Hashes中的这种“有序”行为不一定适用于所有语言,但Ruby documentation状态:Hashes enumerate their values in the order that the corresponding keys were inserted

- 编辑以获得更多通用性

因为,chamnap要求一个更通用的解决方案,它适用于任意数据类型和nil,这里有一个更全面的解决方案,它依赖于<=>运算符:

require 'date'
DIRECTION_MULTIPLIER = { asc: 1, desc: -1 }

# Note: nil will be sorted towards the bottom (regardless if :asc or :desc)
def multi_sort(items, order)
  items.sort do |this, that|
    order.reduce(0) do |diff, order|
      next diff if diff != 0 # this and that have differed at an earlier order entry
      key, direction = order
      # deal with nil cases
      next  0 if this[key].nil? && that[key].nil?
      next  1 if this[key].nil?
      next -1 if that[key].nil?
      # do the actual comparison
      comparison = this[key] <=> that[key]
      next comparison * DIRECTION_MULTIPLIER[direction]
    end
  end
end

我正在使用sort方法。每次sort函数需要与元素进行比较时,都会调用该块。对于相应的对,块应返回-1,0或1(按顺序更小,相等或更高)。在这个排序块中,我将浏览order哈希,其中包含键和项中哈希值的方向。如果我们在顺序中找到了较早的差异(例如,第一个键更高),我们只返回该值。如果过去的比较得出相等的顺序,我们使用<=>运算符来比较传递给排序块的两个元素(如果我们想要降序,则将结果乘以-1)。唯一令人讨厌的事情是处理nil值,这会在实际比较之上增加三行。

这是我的测试代码:

items = [ {n: 'ABC  ', a:   1, b: Date.today+2},
          {n: 'Huhu ', a: nil, b: Date.today-1},
          {n: 'Man  ', a: nil, b: Date.today},
          {n: 'Woman', a: nil, b: Date.today}, 
          {n: 'DEF  ', a:   7, b: Date.today-1}]
multi_sort(items, b: :asc, a: :desc, n: :asc)

更一般地说:由于排序逻辑变得有点复杂,我会将数据用属性包装在实际对象中。然后,您可以覆盖<=>运算符,如here所示。

答案 2 :(得分:1)

解决问题的最简单和最基本的解决方案(如果我能正确理解您的问题)是使用Enumerable#sort_by。例如,如果您想按升序排序按a_sort(在更新的示例中看起来像)字段中的元素:

items.sort_by { |x| x[:a_sort] }

如果您想按两个字段排序,则需要一些用于执行排序的法则。如果我们将这些值转换为字符串,那么最简单的随机定律(无用,但它将作为一个例子)将是字符串连接:

items.sort_by { |x| x[:a_sort].to_s + x[:display_sort].to_s }

如果你想按一个字段排序,然后按另一个字段排序(从你的评论中),你似乎想要分组。如果你想要对比两个字符串更简单的东西进行排序,那么这样的东西是合适的:

items.group_by { |x| x[:a_sort] }
  .sort
  .flat_map do |_, a|
    a.sort_by { |x| x[:display_sort] }
  end

这不是很有效率,但我认为这足以说明我的观点。