Сортировка по нескольким условиям в Ruby - PullRequest
8 голосов
/ 14 апреля 2010

У меня есть коллекция объектов 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!. Какой самый рубиновый способ сделать это?

Ответы [ 2 ]

12 голосов
/ 14 апреля 2010

Вы всегда должны сортировать по одним и тем же критериям, чтобы обеспечить значимый порядок. Если сравнивать две даты 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 Вы также хотите сделать сортировку сразу. Для реализации <=> используйте #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_by и sort_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 (в этом случае вы хотите, чтобы сортировка выполнялась сервером БД).
3 голосов
/ 14 апреля 2010

В качестве альтернативы вы можете выполнить сортировку одним махом в массиве, единственное, что нужно, - это обработать случай, когда один из атрибутов равен nil, хотя это все равно можно обработать, если вы знаете набор данных, выбрав соответствующий nil guard , Также из вашего кода псевдо неясно, перечислены ли сравнения даты и позиции в приоритетном порядке или в том или ином (то есть дата использования, если существует для обеих позиций использования). Первое решение предполагает использование, категория, затем дата, затем позиция

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
...