Search code examples
rubycomparisoncomparison-operators

Any easy way to chain two comparison <=> operators in Ruby?


Ruby comes with the handy <=> comparison operators, and native primitive types support them. I am wondering if there is an easy way to combine them to compare more complex objects, like Structs.For example, given

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]

I would like to sort data by volume and then, if volumes are equal, by code. Like

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

What should I put in ????

I am after a solution that:

  • avoids recalculation of <=>
  • One liner
  • short ;-)

Solution

  • For such a simple case as outlined above, you can use 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>
    #   ]
    

    If you're only sorting by a single attribute, it gets even shorter:

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

    where &:volume uses Symbol#to_proc and is shorthand for proc {|a| a.volume } (similar to lambda).

    If you need to make it more complex (i.e. have different left and right sides), you can expand this to a call to 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>
    #   ]
    

    This whole thing works, because the <=> operator defined on Array does exactly what you need, for arbitrary levels.