在Ruby中按多个条件排序

9

我有一组Post对象,希望能按照以下条件进行排序:

  • 首先按照分类(新闻、事件、实验室、作品集等)排序
  • 然后按照日期排序,如果有日期;如果设置了特定索引,则按照位置排序

一些文章会有日期(新闻和事件),而其他文章则会有明确的位置(实验室和作品集)。

我想要调用posts.sort!,所以我重写了<=>,但我正在寻找按照这些条件进行排序的最有效方法。以下是一个伪代码示例:

def <=>(other)
  # first, everything is sorted into 
  # smaller chunks by category
  self.category <=> other.category

  # then, per category, by date or position
  if self.date and other.date
    self.date <=> other.date
  else
    self.position <=> other.position
  end
end

看起来我需要实际上进行两次排序,而不是把所有东西都塞进那个方法中。类似于sort_by_category,然后是sort!。那么最Ruby的方式是什么?

2个回答

13

为确保有意义的排序,您应始终按相同标准进行排序。如果比较两个nil日期,那么position可以判断它们的顺序,但是如果将一个nil日期与一个设置了日期的日期进行比较,则必须决定哪个日期排在前面,而不考虑其位置(例如通过将nil映射到过去某天来实现)。

否则,想象一下以下情况:

a.date = nil                   ; a.position = 1
b.date = Time.now - 1.day      ; b.position = 2
c.date = Time.now              ; c.position = 0

根据您最初的条件,您会有:a < b < c < a。那么,哪一个是最小的?

您还希望一次完成排序。对于您的<=>实现,请使用#nonzero?

def <=>(other)
  return nil unless other.is_a?(Post)
  (self.category <=> other.category).nonzero? ||
  ((self.date || AGES_AGO) <=> (other.date || AGES_AGO)).nonzero? ||
  (self.position <=> other.position).nonzero? ||
  0
end
如果你只需使用一次比较标准,或者该标准不是通用的,因此不想定义<=>,那么可以使用带有块的sort
post_ary.sort{|a, b| (a.category <=> ...).non_zero? || ... }
更好的是,你可以使用sort_bysort_by!来构建一个数组,用于指定优先级并进行比较:
post_ary.sort_by{|a| [a.category, a.date || AGES_AGO, a.position] }
除了更短之外,使用sort_by的优点在于您可以仅获得一个有序的准则。

注:

  • sort_by!是在Ruby 1.9.2中引入的。 您可以 require 'backports/1.9.2/array/sort_by' 在旧版本的Ruby中使用它。
  • 我假设Post不是ActiveRecord :: Base的子类(如果是,则应该通过数据库服务器进行排序)。

谢谢,我之前不知道Numeric#nonzero?。一个返回非布尔值的?方法是不是有点奇怪? - Mladen Jablanović
@Mladen:是的,但非常有用。另一个例子是你可能期望得到一个“真/假”的地方:String < Fixnum 返回的是 nil,而不是 false - Marc-André Lafortune
这有点误导人: post_ary.sort_by {|a, b| (a.category <=> ...) } sort_by不接受带有两个参数的块。相反,对于更复杂的排序问题,您应该返回一个数组.. 即: post_ary.sort_by {|a| [a.category, a.date, a.position] } - Timo
@TimoLehto:绝对的,感谢指出我的错误。已做出修改。 - Marc-André Lafortune
为什么你有 : .nonzero? || 0?在需要这个的特殊条件下吗?|| 0 - tolbard
@tolbard 没有特别的原因,只是为了展示这个模式。最后一个 nonzero? 和最后一个 || 可以被移除。 - Marc-André Lafortune

4

或者你可以在一个数组中一次性完成排序,唯一需要注意的是处理其中一个属性为nil的情况,但如果你知道数据集,仍然可以通过选择适当的nil保护来处理它。此外,从你的伪代码中无法确定日期和位置比较是按优先级顺序列出的还是其中之一(即如果两个都存在,则使用日期,否则使用位置)。第一种解决方案假定使用类别,其次是日期,最后是位置。

def <=>(other)
    [self.category, self.date, self.position] <=> [other.category, other.date, other.position]
end

Second(第二)假定它是日期或位置

def <=>(other)
    if self.date && other.date
        [self.category, self.date] <=> [other.category, other.date]
    else
        [self.category, self.position] <=> [other.category, other.position]
    end
end

啊,我忘记了日期的 nil。这种排序方式并不是很好(请参见我的更新答案)。 - Marc-André Lafortune
对于我的学习,你说的“不太有序”是什么意思? - naven87
2
对于一个良好的序,以下始终成立:a < b && b < c 意味着 a < c。请参见我的答案,以了解不符合此标准的示例。 - Marc-André Lafortune
但是,通过对第一个案例进行以下微调,就可以得到这个结果: [self.category,self.date || AGES_AGO,self.position] <=> [other.category,other.date || AGES_AGO,other.position] 用您的术语来说,不够优雅,但是没有您额外的检查,结果相同。 - naven87
是的,使用数组这种方式是可以的。不过会比较慢,特别是如果某些字段需要计算(比如 #position 是一个需要进行一些计算的方法),但结果将会相同。 - Marc-André Lafortune

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