Мне нужно создать лямбды для критериев сортировки. Чтобы упростить процесс создания лямбды, я хотел бы упорядочить сравнения последовательно, например:
[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>]
Почему?
Я думаю, вы можете неправильно понять, как работает 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]]
Примечания:
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
[o4, o3, o2, o1]
) неверно, тест проходит. Основываясь на описании теста, ожидаемый результат должен быть [o4, o2, o3, o1]
, а тело лямбды должно измениться на [-a.v1, a.v2] <=> [-b.v1, b.v2]
sort_by
, например. sort_by {|a| [-a.v1,a.v2]}
Сортировка по убыванию завершается ошибкой, если поле сравнения имеет тип Дата с ошибкой undefined method
-@' для #<Date: 2020-01-01 ((2458850j,0s,0n),+0s,2299161j)>`
@MikeSlinn еще один способ сортировки в порядке убывания — поменять местами аргументы ->(a,b) { b<=>a }
Да, это сделали другие. Сначала я сопротивлялся этому подходу из-за дополнительной сложности, необходимой для создания необходимых лямбда-выражений. Однако единственным другим способом, которым я смог заставить эту работу работать с датами, было форматирование в YYYY-MM-DD
, и это имеет аналогичную или худшую сложность.
Обращение аргументов работает только с sort
, но не с sort_by
.
@MikeSlinn правильно, вы не можете использовать sort
для создания убывающего порядка для Date
без преобразования, например. ->(a) { -a.to_time.to_i }
, ->(a) { -a.strftime('%s').to_i }
, ->(a) {-a.jd}
и т. д., потому что Date
не имеет унарной инверсии (-@
)
Для потомков вот ответ с исправлениями @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)>`.
Чтобы увидеть ошибку:
Измените initialize
на:
def initialize(param1, param2)
@v1 = Date.parse(param1)
@v2 = Date.parse(param2)
@v3 = param3
end
Измените определения 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
Так это я правильно понимаю. Я ответил на поставленный вопрос; затем решил вашу проблему с датой в комментариях, и, в свою очередь, вы отметили свой ответ как правильный, отдав мне должное? Просто чтобы быть ясным, я не забочусь об очках репутации, но эта тактика в лучшем случае грубая, граничащая с нападением. Я рад, что помог вам, но ничего себе!
Ваши предложения были правильными. Однако я чувствовал, что этот вопрос, как и все вопросы, заслуживает правильного ответа с полным примером кода. Это разница между предложением и показом того, что вы имеете в виду. Если вы скопируете и вставите мой код или аналогичный код, который вы написали, в свой ответ, я снова проверю ваш ответ.
Хорошо, за исключением того, что это не тот вопрос, который вы задали, и ваш ответ на самом деле не дает никакого контекста без моего. В любом случае, по крайней мере, теперь я понимаю, почему. 👍
Отличная работа! Спасибо за обнаружение ошибок копирования и вставки и отличные объяснения. Ваш комментарий «применить один и тот же унарный с обеих сторон» также был очень полезен. Лямбда для
sort_by
будет проще.