用于按多个参数排序的Ruby Lambda

7cwmlq89  于 2023-02-12  发布在  Ruby
关注(0)|答案(3)|浏览(126)

我需要为排序标准创建lambda。为了简化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]

为了确保lambdas按照我期望的方式工作,我编写了以下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>]

为什么?

zkure5ic

zkure5ic1#

我想你可能误解了Array#<=>(太空船)是如何工作的。(或者这可能只是你测试中的一个打字错误)
它的作用是按a[0] <=> b[0]排序,如果0相等,那么它就移到a[1] <=> b[1],依此类推,直到(x <=> y) != 0;然而,它只会比较一个集合一次,这意味着4个元素4次迭代。这给您带来了一个问题,因为这意味着它永远不会有一个比较集,其中a == o2 && b == o3a == 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.你所有的测试都有一个非常特殊的问题,那就是操作符需要同时应用于两端,例如,* it "sorted by 2 keys,both descending"* 看起来通过了,但这仅仅是由于你给出参数的顺序。

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. it "按2个键排序,第一个键降序,第二个键升序"* 测试错误,但由于您的期望值([o4, o3, o2, o1])不正确,因此测试通过。根据测试描述,预期输出应为[o4, o2, o3, o1],lambda主体应更改为[-a.v1, a.v2] <=> [-b.v1, b.v2]
    1.使用sort_by更容易表达这些测试,例如sort_by {|a| [-a.v1,a.v2]}
flmtquvp

flmtquvp2#

下面是@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类型的属性用作降序排序键,则此解决方案将引发异常。#〈Date:2011年1月1日((2455563 j,0 s,0 n),+0 s,2299161 j)〉'。
要查看错误:
1.将initialize修改为:

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

1.将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] }
kx1ctssn

kx1ctssn3#

前面的答案在根据Date字段应用降序排序时失败。此解决方案使用@ngineersmnky的建议来反转需要降序排序的字段的比较顺序。
请注意,尽管sort_bysort更便于使用,但只有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

相关问题