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

Мне нужно создать лямбды для критериев сортировки. Чтобы упростить процесс создания лямбды, я хотел бы упорядочить сравнения последовательно, например:

[a.v1, b.v2] <=> [a.v1, b.v2]
[a.v1, b.v2] <=> [-a.v1, b.v2]
[a.v1, b.v2] <=> [a.v1, -b.v2]
[a.v1, b.v2] <=> [-a.v1, -b.v2]

Чтобы убедиться, что лямбды работают так, как я ожидаю, я написал следующее rspec:

class Obj
  attr_reader :v1, :v2, :v3

  def initialize(param1, param2, param3)
    @v1 = param1
    @v2 = param2
    @v3 = param3
  end
end

RSpec.describe(Array) do
  let(:o1) { Obj.new(1, 1, 1) }
  let(:o2) { Obj.new(2, 1, 1) }
  let(:o3) { Obj.new(2, 2, 1) }
  let(:o4) { Obj.new(3, 2, 1) }
  let(:objs) { [o1, o2, o3, o4] }

  # See https://ruby-doc.org/3.2.0/Comparable.html
  it "uses comparators properly" do
    expect([o1.v1] <=> [o2.v1]).to eq(-1)
    expect([o1.v1, o1.v2] <=> [o2.v1, o2.v2]).to eq(-1)
    expect([o1.v2, o1.v1] <=> [o3.v1, o3.v2]).to eq(-1)
    expect([o1.v2, o1.v1] <=> [o3.v1, -o3.v2]).to eq(-1)
    expect([o2.v2, o1.v1] <=> [-o2.v2, o2.v1]).to eq(1)
    expect([o2.v2, o1.v1] <=> [-o2.v2, -o2.v1]).to eq(1)
  end

  # See https://ruby-doc.org/3.2.0/Enumerable.html#method-i-sort
  it "sorts by 2 keys, both ascending" do
    sort_lambda = ->(a, b) { [a.v1, a.v2] <=> [b.v1, b.v2] }
    result = objs.sort(&sort_lambda)
    expect(result).to eq([o1, o2, o3, o4])
  end

  it "sorts by 2 keys, both descending" do
    sort_lambda = ->(a, b) { [a.v1, a.v2] <=> [-b.v1, -b.v2] }
    result = objs.sort(&sort_lambda)
    expect(result).to eq([o4, o3, o2, o1])
  end

  it "sorts by 2 keys, first descending and second ascending" do
    sort_lambda = ->(a, b) { [a.v1, b.v2] <=> [-a.v1, b.v2] }
    result = objs.sort(&sort_lambda)
    expect(result).to eq([o4, o3, o2, o1])
  end

  # This one fails ... why?
  it "sorts by 2 keys, first ascending and second descending" do
    sort_lambda = ->(a, b) { [a.v1, b.v2] <=> [a.v1, -b.v2] }
    result = objs.sort(&sort_lambda)
    expect(result).to eq([o1, o3, o2, o4])
  end
end

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

Failure/Error: expect(result).to eq([o1, o3, o2, o4])

expected: [#<Obj:0x00007fc2ac112940 @v1=1, @v2=1, @v3=1>, #<Obj:0x00007fc2ac112828 @v1=2, @v2=2, @v3=1>, #<Obj:0x00007fc2ac1128c8 @v1=2, @v2=1, @v3=1>, #<Obj:0x00007fc2ac112788 @v1=3, @v2=2, @v3=1>]
     got: [#<Obj:0x00007fc2ac112788 @v1=3, @v2=2, @v3=1>, #<Obj:0x00007fc2ac112828 @v1=2, @v2=2, @v3=1>, #<Obj:0x00007fc2ac1128c8 @v1=2, @v2=1, @v3=1>, #<Obj:0x00007fc2ac112940 @v1=1, @v2=1, @v3=1>]

Почему?

Пошаговое руководство по созданию собственного Slackbot: От установки до развертывания
Пошаговое руководство по созданию собственного Slackbot: От установки до развертывания
Шаг 1: Создание приложения Slack Чтобы создать Slackbot, вам необходимо создать приложение Slack. Войдите в свою учетную запись Slack и перейдите на...
0
0
95
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Я думаю, вы можете неправильно понять, как работает Array#<=> (космический корабль). (или, может быть, это просто опечатка в ваших тестах)

Это сортирует по a[0] <=> b[0], и если это 0 (равно), то он переходит к a[1] <=> b[1] и так далее до (x <=> y) != 0; однако он будет сравнивать набор только один раз, что означает 4 элемента 4 итерации. Это вызывает у вас проблему, потому что это означает, что у него никогда не будет набора сравнений, где a == o2 && b == o3, а затем также a == o3 && b == o2

В вашем неудачном тесте:

sort_lambda = ->(a, b) { [a.v1, b.v2] <=> [a.v1, -b.v2] }

a.v1 всегда будет равно a.v1 например. верните 0, чтобы перейти к сравнению b.v2 с -b.v2, и вы получите b.v2 в порядке убывания.

Похоже, что вы на самом деле хотели

sort_lambda = ->(a, b) { [a.v1, -a.v2] <=> [b.v1, -b.v2] }

Пример:

a = [[1,1,1],[2,1,1],[2,2,1],[3,2,1]]
sort_lambda = ->(a, b) { [a.v1, -a.v2] <=> [b.v1, -b.v2] }
a.sort(&sort_lambda)
#=> [[1, 1, 1], [2, 2, 1], [2, 1, 1], [3, 2, 1]]

Примечания:

  1. Все ваши тесты страдают от очень специфической проблемы, заключающейся в том, что оператор должен применяться к обеим сторонам. Например, кажется, что "сортировка по двум ключам, оба по убыванию", проходит, но это происходит только из-за порядка, в котором вы представили аргументы. например
sort_lambda = ->(a, b) { [a[0], a[1]] <=> [-b[0], -b[1]] }
a.sort(&sort_lambda)
#=> [[3, 2, 1], [2, 2, 1], [2, 1, 1], [1, 1, 1]] #looks like it works 
b = [a[0],a[2],a[1],a[3]] 
b.sort(&sort_lambda)
#=> [[3, 2, 1], [2, 1, 1], [2, 2, 1], [1, 1, 1]] # Oh No 
b.sort(&sort_lambda) == a.sort(&sort_lambda) #=> false
# apply the same unary on both sides 
sort_lambda = ->(a, b) { [-a[0], -a[1]] <=> [-b[0], -b[1]] }
b.sort(&sort_lambda)
#=> [[3, 2, 1], [2, 2, 1], [2, 1, 1], [1, 1, 1]] # that's better 
b.sort(&sort_lambda) == a.sort(&sort_lambda) #=> true 
  1. Тест «сортирует по 2 ключам, первый по убыванию и второй по возрастанию» неверен, но поскольку ваше ожидание ([o4, o3, o2, o1]) неверно, тест проходит. Основываясь на описании теста, ожидаемый результат должен быть [o4, o2, o3, o1], а тело лямбды должно измениться на [-a.v1, a.v2] <=> [-b.v1, b.v2]
  2. Эти тесты легче выразить с помощью sort_by, например. sort_by {|a| [-a.v1,a.v2]}

Отличная работа! Спасибо за обнаружение ошибок копирования и вставки и отличные объяснения. Ваш комментарий «применить один и тот же унарный с обеих сторон» также был очень полезен. Лямбда для sort_by будет проще.

Mike Slinn 07.02.2023 19:46

Сортировка по убыванию завершается ошибкой, если поле сравнения имеет тип Дата с ошибкой undefined method -@' для #<Date: 2020-01-01 ((2458850j,0s,0n),+0s,2299161j)>`

Mike Slinn 08.02.2023 00:56

@MikeSlinn еще один способ сортировки в порядке убывания — поменять местами аргументы ->(a,b) { b<=>a }

engineersmnky 08.02.2023 03:46

Да, это сделали другие. Сначала я сопротивлялся этому подходу из-за дополнительной сложности, необходимой для создания необходимых лямбда-выражений. Однако единственным другим способом, которым я смог заставить эту работу работать с датами, было форматирование в YYYY-MM-DD, и это имеет аналогичную или худшую сложность.

Mike Slinn 08.02.2023 14:51

Обращение аргументов работает только с sort, но не с sort_by.

Mike Slinn 08.02.2023 15:27

@MikeSlinn правильно, вы не можете использовать sort для создания убывающего порядка для Date без преобразования, например. ->(a) { -a.to_time.to_i }, ->(a) { -a.strftime('%s').to_i }, ->(a) {-a.jd} и т. д., потому что Date не имеет унарной инверсии (-@)

engineersmnky 08.02.2023 17:21

Для потомков вот ответ с исправлениями @engineersmnky. Для каждого сценария предусмотрен как sort, так и эквивалентный sort_by подход.

class Obj
  attr_reader :v1, :v2, :v3

  def initialize(param1, param2, param3)
    @v1 = param1
    @v2 = param2
    @v3 = param3
  end
end

RSpec.describe(Array) do # rubocop:disable Metrics/BlockLength
  let(:o1) { Obj.new(1, 1, 1) }
  let(:o2) { Obj.new(2, 1, 1) }
  let(:o3) { Obj.new(2, 2, 1) }
  let(:o4) { Obj.new(3, 2, 1) }
  let(:objs) { [o1, o2, o3, o4] }

  # See https://ruby-doc.org/3.2.0/Comparable.html
  it "uses comparators properly" do
    expect([o1.v1] <=> [o2.v1]).to eq(-1)
    expect([o1.v1, o1.v2] <=> [o2.v1, o2.v2]).to eq(-1)
    expect([o1.v2, o1.v1] <=> [o3.v1, o3.v2]).to eq(-1)
    expect([o1.v2, o1.v1] <=> [o3.v1, -o3.v2]).to eq(-1)
    expect([o2.v2, o1.v1] <=> [-o2.v2, o2.v1]).to eq(1)
    expect([o2.v2, o1.v1] <=> [-o2.v2, -o2.v1]).to eq(1)
  end

  # See https://ruby-doc.org/3.2.0/Enumerable.html#method-i-sort
  it "sort with 2 keys, both ascending" do
    sort_lambda = ->(a, b) { [a.v1, a.v2] <=> [b.v1, b.v2] }
    result = objs.sort(&sort_lambda)
    expect(result).to eq([o1, o2, o3, o4])
  end

  it "sort_by with 2 keys, both ascending" do
    sort_lambda = ->(a) { [a.v1, a.v2] }
    result = objs.sort_by(&sort_lambda)
    expect(result).to eq([o1, o2, o3, o4])
  end

  it "sort with 2 keys, both descending" do
    sort_lambda = ->(a, b) { [-a.v1, -a.v2] <=> [-b.v1, -b.v2] }
    result = objs.sort(&sort_lambda)
    expect(result).to eq([o4, o3, o2, o1])
  end

  it "sort_by with 2 keys, both descending" do
    sort_lambda = ->(a) { [-a.v1, -a.v2] }
    result = objs.sort_by(&sort_lambda)
    expect(result).to eq([o4, o3, o2, o1])
  end

  it "sort with 2 keys, first descending and second ascending" do
    sort_lambda = ->(a, b) { [-a.v1, a.v2] <=> [-b.v1, b.v2] }
    result = objs.sort(&sort_lambda)
    expect(result).to eq([o4, o2, o3, o1])
  end

  it "sort_by with 2 keys, first descending and second ascending" do
    sort_lambda = ->(a) { [-a.v1, a.v2] }
    result = objs.sort_by(&sort_lambda)
    expect(result).to eq([o4, o2, o3, o1])
  end

  it "sort with 2 keys, first ascending and second descending" do
    sort_lambda = ->(a, b) { [a.v1, -a.v2] <=> [b.v1, -b.v2] }
    result = objs.sort(&sort_lambda)
    expect(result).to eq([o1, o3, o2, o4])
  end

  it "sort_by with 2 keys, first ascending and second descending" do
    sort_lambda = ->(a) { [a.v1, -a.v2] }
    result = objs.sort_by(&sort_lambda)
    expect(result).to eq([o1, o3, o2, o4])
  end
end

Это решение создает исключение, если свойство типа Date используется в качестве ключа сортировки по убыванию. Сообщение об ошибке вида undefined method -@' for #<Date: 2011-01-01 ((2455563j,0s,0n),+0s,2299161j)>`.

Чтобы увидеть ошибку:

  1. Измените initialize на:

    def initialize(param1, param2)
      @v1 = Date.parse(param1)
      @v2 = Date.parse(param2)
      @v3 = param3
    end
    
  2. Измените определения o1 .. o4, чтобы они имели форму:

    let(:o1) { Obj.new('2000-01-01', '2001-01-01', 1) }
    let(:o2) { Obj.new('2010-01-01', '2001-01-01', 1) }
    let(:o3) { Obj.new('2010-01-01', '2011-01-01', 1) }
    let(:o4) { Obj.new('2020-01-01', '2011-01-01', 1) }
    let(:objs) { [o1, o2, o3, o4] }
    
Ответ принят как подходящий

Предыдущие ответы терпят неудачу при применении сортировки по убыванию в соответствии с полем (полями) Date. В этом решении используется предложение @ngineersmnky, чтобы изменить порядок сравнения для полей, которые необходимо отсортировать в порядке убывания.

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

class Obj
  # `date_modified` is primary sort key
  # `date` (when specified) is secondary sort key
  attr_reader :date, :date_modified

  def initialize(param1, param2)
    @date_modified = Date.parse(param1)
    @date = Date.parse(param2)
  end
end

RSpec.describe(Obj) do
  let(:o1) { Obj.new('2000-01-01', '2001-01-01') }
  let(:o2) { Obj.new('2010-01-01', '2001-01-01') }
  let(:o3) { Obj.new('2010-01-01', '2011-01-01') }
  let(:o4) { Obj.new('2020-01-01', '2011-01-01') }
  let(:objs) { [o1, o2, o3, o4] }

  # See https://ruby-doc.org/3.2.0/Comparable.html
  it "compares one key with ascending dates" do
    expect([o1.date_modified] <=> [o2.date_modified]).to eq(-1)
    expect([o2.date_modified] <=> [o3.date_modified]).to eq(0)
    expect([o3.date_modified] <=> [o4.date_modified]).to eq(-1)
  end

  it "compares two keys with ascending dates" do
    expect([o1.date_modified, o1.date] <=> [o2.date_modified, o2.date]).to eq(-1)
    expect([o2.date_modified, o2.date] <=> [o3.date_modified, o3.date]).to eq(-1)
    expect([o3.date_modified, o3.date] <=> [o4.date_modified, o4.date]).to eq(-1)
  end

  it "compares one key with descending dates" do
    expect([o1.date_modified] <=> [o2.date_modified]).to eq(-1)
    expect([o2.date_modified] <=> [o3.date_modified]).to eq(0)
  end

  it "compares two keys with descending dates" do
    expect([o2.date_modified, o2.date] <=> [o1.date_modified, o1.date]).to eq(1)
    expect([o3.date_modified, o3.date] <=> [o2.date_modified, o2.date]).to eq(1)
    expect([o4.date_modified, o4.date] <=> [o3.date_modified, o3.date]).to eq(1)
  end

  # See https://ruby-doc.org/3.2.0/Enumerable.html#method-i-sort
  it "sort with both keys ascending" do
    sort_lambda = ->(a, b) { [a.date_modified, a.date] <=> [b.date_modified, b.date] }
    result = objs.sort(&sort_lambda)
    expect(result).to eq([o1, o2, o3, o4])
  end

  it "sort with both keys descending" do
    sort_lambda = ->(a, b) { [b.date_modified, b.date] <=> [a.date_modified, a.date] }
    result = objs.sort(&sort_lambda)
    expect(result).to eq([o4, o3, o2, o1])
  end

  it "sort with date_modified descending and date ascending" do
    sort_lambda = ->(a, b) { [b.date_modified, a.date] <=> [a.date_modified, b.date] }
    result = objs.sort(&sort_lambda)
    expect(result).to eq([o4, o2, o3, o1])
  end

  it "sort with date_modified ascending and date descending" do
    sort_lambda = ->(a, b) { [a.date_modified, b.date] <=> [b.date_modified, a.date] }
    result = objs.sort(&sort_lambda)
    expect(result).to eq([o1, o3, o2, o4])
  end
end

Так это я правильно понимаю. Я ответил на поставленный вопрос; затем решил вашу проблему с датой в комментариях, и, в свою очередь, вы отметили свой ответ как правильный, отдав мне должное? Просто чтобы быть ясным, я не забочусь об очках репутации, но эта тактика в лучшем случае грубая, граничащая с нападением. Я рад, что помог вам, но ничего себе!

engineersmnky 10.02.2023 01:39

Ваши предложения были правильными. Однако я чувствовал, что этот вопрос, как и все вопросы, заслуживает правильного ответа с полным примером кода. Это разница между предложением и показом того, что вы имеете в виду. Если вы скопируете и вставите мой код или аналогичный код, который вы написали, в свой ответ, я снова проверю ваш ответ.

Mike Slinn 10.02.2023 13:22

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

engineersmnky 11.02.2023 01:14

Другие вопросы по теме