【问题标题】:Sorting By Multiple Conditions in Ruby在 Ruby 中按多个条件排序
【发布时间】:2011-02-07 16:56:27
【问题描述】:

我有一个 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 sorting operators comparison-operators spacecraft-operator


    【解决方案1】:

    您应该始终按照相同的标准进行排序,以确保排序有意义。如果比较两个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

    您还想立即进行排序。对于您的&lt;=&gt; 实现,请使用#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
    

    如果您只使用一次比较标准,或者如果该标准不是通用的,因此不想定义 &lt;=&gt;,您可以将 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' 将其与旧版红宝石一起使用。
    • 我假设Post 不是ActiveRecord::Base 的子类(在这种情况下,您希望由数据库服务器完成排序)。

    【讨论】:

    • 谢谢,我不知道Numeric#nonzero??-方法返回非布尔值是不是有点奇怪?
    • @Mladen:确实有用,但非常有用。另一个你可以期待true/false的例子:String &lt; Fixnum返回nil,而不是false
    • 这有点误导:post_ary.sort_by {|a, b| (a.category &lt;=&gt; ...) } sort_by 不接受带有两个参数的块。相反,对于更复杂的排序问题,您应该返回一个数组。即:post_ary.sort_by {|a| [a.category, a.date, a.position] }
    • @TimoLehto:当然,感谢您指出我的错误。相应地进行了编辑。
    • 为什么你有:.nonzero? || 0 需要这个的时候有特殊条件吗|| 0
    【解决方案2】:

    或者,您可以一举在一个数组中进行排序,唯一的问题是处理其中一个属性为 nil 的情况,尽管如果您通过选择适当的 nil 保护知道数据集,仍然可以处理这种情况.此外,从您的伪代码中也不清楚日期和位置比较是按优先顺序还是按优先顺序列出的(即,如果两者都存在,则使用日期)。第一个解决方案假设使用,类别,后跟日期,然后是位置

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

    第二个假设是日期或位置

    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。这种排序顺序不是很好(请参阅我更新的答案)。
    • 对于我的学习,你说的不是很好是什么意思?
    • 对于有根据的订单,以下内容始终成立:a &lt; b &amp;&amp; b &lt; c 暗示 a &lt; c。请参阅我的答案以获取不符合此标准的示例。
    • 但是您可以通过对第一种情况进行以下调整来获得它:[self.category, self.date || AGES_AGO, self.position] [other.category, other.date || AGES_AGO, other.position] 使用您的命名法,对,没有那么优雅,但没有您的额外检查结果相同
    • 是的,以这种方式使用数组很好。虽然它会慢一些,特别是如果需要计算某些字段(比如#position 是一种进行一些计算的方法),但会产生相同的结果。
    猜你喜欢
    • 2013-09-13
    • 1970-01-01
    • 1970-01-01
    • 2021-07-04
    • 2012-02-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-11
    相关资源
    最近更新 更多