Сортировка по нескольким условиям в Ruby

У меня есть коллекция объектов 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 ответа

Решение

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

В качестве альтернативы вы можете выполнить сортировку одним махом в массиве, единственное, что нужно, - это обработать случай, когда один из атрибутов равен 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
Другие вопросы по тегам