Nice way to define a comparision method for a struct

I recently came across the problem of having to sort an array, where the
members are objects of a Struct type, and I need to define a custom
comparision function on certain elements of this type. To illustrate the
problem, think of having

Customer = Struct.new(:custno, :first_name, :family_name, :title,

:addr1, :addr2, :country, :and, :many, :more)

and that I have an array ‘customers’ of elements of type Customer, and I
would like to produce a sorted version of the array, sorted first by
:family_name, then by :title, then by :custno, and finally by
:first_name. To start with:

sorted_customers = customers.sort_by {|left,right|

cust_comp(left,right)}

The question is: How can we define cust_comp, so that it looks nice,
understandable and maintainable?

Here is one approach (which I don’t really like):

SORT_FIELDS=%s(family_name, title, custno, first_name)
def cust_comp(a,b)
  result=0
  SORT_FIELDS.detect do |field|
    result = a.send(:field) <=> b.send(:field)
    result != 0
  end
  result
end

It is ugly, because of the need of the “outside” variable ‘result’ to
keep track of the result so far.

Any idea how to write this in a nicer way?

You ask very interesting questions, Ronald.

I don’t think there is a simple answer. My first attempt would be to
define <=> on the struct:

Customer = Struct.new(:cust_no, :first_name, :family_name, :title) do
def <=>(other)
[ family_name, first_name, cust_no ] <=>
[ other.family_name, other.first_name, other.cust_no ]
end
end

I wouldn’t do it on <=>, because I want to have the option of defining
several comparision strategies, but this is not important here. Indeed,
I like your solution, in that it is shorter, and at the same time I
dislike it, because it requires to name the fields twice in the exactly
same sequence, which is not so good for maintenance, and also because it
requires the construction of two temporary arrays for each comparision.

But if I am willing to ignore the cost of additional array creation, I
could maybe adapt your solution like this:

Customer = … do
def custom_sort_order
[ family_name, …]
end
end

and then sort like this:

array.sort_by {|left,right| left.custom_sort_order <=>
right.custom_sort_order }

That’s true, I think the original solution is very interesting even more
so after you’re explaination-- and it lets you vary the elements in the
struct easily, as well.

you guys use sort_by wrong.

if you use sort_BY when you have no left and right side,
just

array.sort_by(&:custom_sort_order)

does the job.

Hans M. wrote in post #1184812:

you guys use sort_by wrong.

if you use sort_BY when you have no left and right side,

Indeed, I just found out 10 minutes ago when debugging my code!!

This is the way of the ruby - there is almost always a shorter, prettier
way!