Есть простой способ связать два оператора сравнения в Ruby?

#ruby #comparison #comparison-operators

#ruby #сравнение #сравнение-операторы

Вопрос:

Ruby поставляется с удобными <=> операторами сравнения, и их поддерживают собственные примитивные типы. Мне интересно, есть ли простой способ объединить их для сравнения более сложных объектов, таких как структуры.Например, учитывая

 class Datum < Struct.new(:code, :volume); end

datum1 = Datum.new('B',10)
datum2 = Datum.new('A',10)
datum3 = Datum.new('C',11)

data = [datum1, datum2, datum3]
  

Я хотел бы отсортировать data по volume , а затем, если volume s равны, по code . Нравится

 data.sort {|a,b| (a.volume <=> b.volume) ??? (a.code <=> b.code)}
  

Что я должен вставить ??? ?

Я ищу решение, которое:

  • позволяет избежать пересчета <=>
  • Один вкладыш
  • короткий 😉

Ответ №1:

Для такого простого случая, как описано выше, вы можете использовать sort_by :

 data.sort_by {|a| [a.volume, a.code] }
#=> [
#     #<struct Datum code="A", volume=10>,
#     #<struct Datum code="B", volume=10>,
#     #<struct Datum code="C", volume=11>
#   ]
  

Если вы сортируете только по одному атрибуту, он становится еще короче:

 data.sort_by(amp;:volume)
#=> [
#     #<struct Datum code="B", volume=10>,
#     #<struct Datum code="A", volume=10>,
#     #<struct Datum code="C", volume=11>
#   ]
  

где amp;:volume используется Symbol#to_proc и является сокращением для proc {|a| a.volume } (аналогично lambda).

Если вам нужно сделать его более сложным (т. Е. Иметь разные левую и правую стороны), вы можете расширить это до вызова sort :

 data.sort {|a,b| [a.volume, a.code] <=> [b.volume, b.code] }
#=> [
#     #<struct Datum code="A", volume=10>,
#     #<struct Datum code="B", volume=10>,
#     #<struct Datum code="C", volume=11>
#   ]
  

Все это работает, потому <=> что оператор, определенный на Array , делает именно то, что вам нужно, для произвольных уровней.

Комментарии:

1. Я попросил однострочный вариант и data.sort {|a,b| [a.volume, a.code] <=> [b.volume, b.code] } , конечно же, спасибо @p11y!

Ответ №2:

Я думаю, вы идете по этому неправильному пути. Почему бы вам не избавить себя от хлопот и не добавить <=> оператор в Datum ? Что-то вроде этого:

 class Datum < Struct.new(:code, :volume)
  def <=>(other)
    if(self.code < other.code)
      -1
    elsif(self.code > other.code)
      1
    elsif(self.volume < other.volume)
      -1
    elsif(self.volume > other.volume)
      1
    else
      0
    end
  end
end
  

Тогда вы можете сказать data.sort и покончить с этим. И как только у вас есть <=> оператор, вы можете include Comparable и получить < , <= , … операторы бесплатно.

Ответ №3:

Способ Ruby для объединения компараторов в цепочку заключается в использовании Numeric.nonzero? :

 data.sort {|a,b| (a.volume <=> b.volume).nonzero? || a.code <=> b.code}
  

Что Numeric.nonzero? делать? Он возвращает число в случае, если оно отличается от нуля, в противном случае оно возвращается nil . Затем || оператор заботится обо всем остальном, поскольку nil обрабатывается как false в логическом контексте, таким образом nil || b , является таким же, как b .

При этом в большинстве ситуаций вы получите более короткий код при использовании Enumerable.sort_by и вернете массив для указания порядка сортировки:

 data.sort_by {|a| [a.volume, a.code] }
  

Также проще изменить порядок сортировки, чем при использовании компаратора:

 data.sort {|a,b| (-(a.volume <=> b.volume)).nonzero? || a.code <=> b.code}
# vs.
data.sort_by {|a| [-a.volume, a.code] }
  

Внимание: имейте в виду, что sort_by это медленнее, чем sort в случае, если компаратор работает быстро, и, наоборот, sort_by лучше, если компаратор работает медленно.