сортировка и перестановка массива хэшей на основе нескольких условий - PullRequest
0 голосов
/ 07 сентября 2018

Я пытаюсь отсортировать массив на основе 3 различных критериев. Допустим, у меня есть массив хэшей как это:

a = [
    { "name" => "X", "year" => "2013-08"},
    { "name" => "A", "year" => "2017-01"},
    { "name" => "X", "year" => "2000-08"},
    { "name" => "B", "year" => "2018-05"},
    { "name" => "D", "year" => "2016-04"},
    { "name" => "C", "year" => "2016-04"}
]

Я хотел бы отсортировать все элементы сначала по «году» в порядке убывания, затем по «имени» в порядке возрастания, а затем переместить все элементы, соответствующие данному имени, в начало массива, сохраняя при этом «год» порядок. Для этого примера, скажем, я ищу элементы со значением «name», равным «X». Поэтому вывод, который я ищу, будет:

{"name"=>"X", "year"=>"2013-08"}
{"name"=>"X", "year"=>"2000-08"}
{"name"=>"B", "year"=>"2018-05"}
{"name"=>"A", "year"=>"2017-01"}
{"name"=>"C", "year"=>"2016-04"}
{"name"=>"D", "year"=>"2016-04"}

Так что все в порядке убывания «года», затем в порядке возрастания «имени», затем все хэши, где «имя» == «Х» переместилось наверх, все еще отсортированные по «году».

Я позаботился о сортировке по возрастанию / убыванию, выполнив следующее:

a.sort { |a,b| [b["year"], a["name"]] <=> [a["year"], b["name"]] }        

Но это касается только первых двух критериев того, что мне нужно. Я попробовал что-то подобное потом:

top = []
a.each { |x| top << x if x["name"] == "X" }
a.delete_if { |x| x["name"] == "X"}
a.unshift(top)

, который производит желаемый результат, но, очевидно, неуклюж и не кажется лучшим способом сделать что-то. Есть ли более быстрый и эффективный способ сделать то, что я пытаюсь сделать?

(К вашему сведению, значения года - это строки, и я не могу преобразовать их в целые числа. Здесь я упростил значения, но данные, из которых я извлекаю данные, фактически добавляют ряд других символов и символов в конце каждого значения .)

Ответы [ 4 ]

0 голосов
/ 07 сентября 2018

Вот еще один вариант, использующий то, что у вас уже есть в качестве базы (поскольку вы были в основном там)

a = [
  { "name" => "X", "year" => "2013-08"},
  { "name" => "A", "year" => "2017-01"},
  { "name" => "X", "year" => "2000-08"},
  { "name" => "B", "year" => "2018-05"},
  { "name" => "D", "year" => "2016-04"},
  { "name" => "C", "year" => "2016-04"}
]


a.sort do  |a,b| 
  a_ord, b_ord = [a,b].map {|e| e["name"] == "X" ? 0 : 1 }
  [a_ord,b["year"],a["name"] ] <=> [b_ord, a["year"],b["name"]]
end

Здесь мы просто проверяем, что «X» всегда впереди, присваивая ему 0, а все остальное - 1. Тогда, поскольку 0 и 0 будут эквивалентны, X вернется к той же логике, которую вы уже применили, как и все другие. Мы можем сделать это немного изящнее, как:

a.sort do  |a,b| 
  [a,b].map {|e| e["name"] == "X" ? 0 : 1 }.zip(
    [b["year"],a["year"]],[a["name"],b["name"]]
  ).reduce(:<=>)
end
0 голосов
/ 07 сентября 2018
arr = [
  {"name"=>"X", "year"=>"2013-08"},
  {"name"=>"X", "year"=>"2000-08"},
  {"name"=>"B", "year"=>"2018-05"},
  {"name"=>"A", "year"=>"2017-01"},
  {"name"=>"C", "year"=>"2016-04"},
  {"name"=>"D", "year"=>"2016-04"},
]

Когда части массива должны быть отсортированы иначе, чем другие части массива, я считаю целесообразным разделить массив на связанные части, отсортировать каждую часть отдельно, а затем объединить результаты этих сортировок. Такой подход не только прост для читателей, но и упрощает тестирование и, как правило, не менее эффективен, чем выполнение одного более сложного вида. Здесь мы разделим массив на две части.

x, non_x = arr.partition { |h| h["name"] == 'X' }
  #=> [[{"name"=>"X", "year"=>"2013-08"}, {"name"=>"X", "year"=>"2000-08"}],
  #    [{"name"=>"B", "year"=>"2018-05"}, {"name"=>"A", "year"=>"2017-01"},
  #     {"name"=>"C", "year"=>"2016-04"}, {"name"=>"D", "year"=>"2016-04"}]]

Сортировать массив x просто.

sorted_x = x.sort_by { |h| h["year"] }.reverse
  #=> [{"name"=>"X", "year"=>"2013-08"}, {"name"=>"X", "year"=>"2000-08"}]

Сортировка non_x является более сложной, потому что она должна быть отсортирована по уменьшению порядка значений "year", а связи должны быть разорваны по значениям "name" в увеличение порядка. В этой ситуации мы всегда можем использовать Array # sort .

non_x.sort do |g,h|
  case g["year"] <=> h["year"]
  when -1
    1
  when 1
    -1
  when 0
    (g["name"] < h["name"]) ? -1 : 1
  end
end
  #=> [{"name"=>"B", "year"=>"2018-05"}, {"name"=>"A", "year"=>"2017-01"},
  #    {"name"=>"C", "year"=>"2016-04"}, {"name"=>"D", "year"=>"2016-04"}]

Приложив немного усилий, мы могли бы альтернативно использовать Enumerable # sort_by . Учитывая хэш h, нам нужно отсортировать либо

[h["year"], f(h["name"])].reverse

, где f - это метод, который приводит к сортировке h["name"] в порядке убывания или (примечание .reverse в дальнейшем)

[f(h["year"]), h["name"]]

, где f - это метод сортировки h["year"] в порядке убывания. Последнее легче реализовать из двух. Мы могли бы использовать следующий метод.

def year_str_to_int(year_str)
  yr, mon = year_str.split('-').map(&:to_i)
  12 * yr + mon
end

Это позволяет нам сортировать non_x по желанию:

sorted_non_x = non_x.sort_by { |h| [-year_str_to_int(h["year"]), h["name"]] }
  #=> [{"name"=>"B", "year"=>"2018-05"}, {"name"=>"A", "year"=>"2017-01"},
  #    {"name"=>"C", "year"=>"2016-04"}, {"name"=>"D", "year"=>"2016-04"}]

Теперь мы просто объединяем два отсортированных раздела.

sorted_x.concat(sorted_non_x)
  #=> [{"name"=>"X", "year"=>"2013-08"}, {"name"=>"X", "year"=>"2000-08"},
  #    {"name"=>"B", "year"=>"2018-05"}, {"name"=>"A", "year"=>"2017-01"}, 
  #    {"name"=>"C", "year"=>"2016-04"}, {"name"=>"D", "year"=>"2016-04"}]
0 голосов
/ 07 сентября 2018

Вы можете написать свой собственный компаратор, реализовав сопоставимую логику для ваших объектов, например так:

require 'pp'

a = [
    { "name" => "X", "year" => "2013-08"},
    { "name" => "A", "year" => "2017-01"},
    { "name" => "X", "year" => "2000-08"},
    { "name" => "B", "year" => "2018-05"},
    { "name" => "D", "year" => "2016-04"},
    { "name" => "C", "year" => "2016-04"}
]

class NameYearSorter
  attr_reader :value
  def initialize(value)
    @value = value
  end

  def name
    value['name']
  end

  def year
    value['year']
  end

  def <=>(other)
    if self.name != 'X' && other.name != 'X'
      if self.year == other.year
        self.name <=> other.name
      else
        self.year > other.year ? -1 : 0
      end
    elsif self.name == 'X' && other.name != 'X'
      -1
    elsif other.name == 'X' && self.name != 'X'
      0   
    elsif self.name == other.name
      other.year > self.year ? 0 : -1
    end
  end
end

sortable = a.map{ |v| NameYearSorter.new(v) }
pp sortable.sort.map(&:value)

# Output:
#=> [{"name"=>"X", "year"=>"2013-08"},
#=>  {"name"=>"X", "year"=>"2000-08"},
#=>  {"name"=>"B", "year"=>"2018-05"},
#=>  {"name"=>"A", "year"=>"2017-01"},
#=>  {"name"=>"C", "year"=>"2016-04"},
#=>  {"name"=>"D", "year"=>"2016-04"}]
0 голосов
/ 07 сентября 2018

sort - это не то, что вы хотите использовать, если у вас есть согласованные критерии сортировки. Более быстрый метод - sort_by:

a.sort_by { |e| [ e["year"], e["name"] ] }

Так как вы хотите их в обратном порядке:

a.sort_by { |e| [ e["year"], e["name"] ] }.reverse

Если в действительности каждый элемент в массиве сортируется на основе преобразованной формы, выраженной в блоке, то вместо этого производится сортировка на основе этих элементов. Это преобразование выполняется один раз и только один раз, и это намного менее грязно, чем метод sort, который должен выполнять это преобразование каждый раз, когда производится сравнение.

Теперь, если вы хотите отсортировать записи «X» сверху, вы можете легко добавить это в качестве дополнительного критерия:

a.sort_by { |e| [ e["name"] == "X" ? 1 : 0, e["year"], e["name"] ] }.reverse

Так что это приведет вас туда, где вы хотите быть.

Хорошая особенность sort_by в том, что вы обычно можете выразить действительно сложную логику сортировки в виде последовательности элементов в массиве. При условии, что каждый элемент сопоставим, все работает.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...