Chibi Canton Chibi Canton - 1 year ago 60
Ruby Question

Ruby Array Sort 2 different ways

I have an array of objects that I'm trying to sort by multiple criteria. Most of the comparisons are just by doing

on their hashes, so using
is very fast, but one of them is more complex.

The array is of soccer teams, and it's currently being sorted like this:

teams.sort_by { |item| [item.points, item.goal_dif, item.goals] }

However, in case at last 2 teams have identical values on those 3 fields, I want the tiebreaker to be a function that I made,
a_beat_b(teamA, teamB)

I tried using
, but it's extremely slow compared to
for those first few... My implementation was like this:

teams.sort ( |a,b| [a.points, a.goals_dif, a.goals] <=> [b.points, b.goals_dif, b.goals])

It was very slow compared to sort_by. The functions for points, goals_dif and goals require some simple queries, but it gets bogged down if it has to do hundreds.

I'm not very good at Ruby, so not sure where to put my
in there. (It returns 1, 0 or -1 if A beat, drew or lost to B, repsectively)

Answer Source

I tried using Array.sort, but it's extremely slow compared to sort_by for those first few

This is because sort calls the given block several times. Here's an example to show what's going on under the hood: (sorting "apple", "pear" and "fig" by length)

def length(str)
  puts "calculating #{str.inspect}.length"

array = %w{apple pear fig}
array.sort { |a, b| length(a) <=> length(b) }
#=> ["fig", "pear", "apple"]

Output from our length method:

calculating "apple".length
calculating "pear".length
calculating "apple".length
calculating "fig".length
calculating "pear".length
calculating "fig".length

As you can see, length is called multiple times during the sort. Imagine that these are database queries.

sort_by on the other hand calls the block once for each element, building an internal mapping:

array.sort_by { |a| length(a) }
#=> ["fig", "pear", "apple"]


calculating "apple".length
calculating "pear".length
calculating "fig".length

For expensive operations (like database queries), this is much faster. But it's also less flexible – you can't dynamically compare a and b any more.

You can however store the results of your (expensive) operation, for example by using a hash: (this is called memoization)

hash = { |h, k| h[k] = length(k) }

And use the hash within sort:

array.sort { |a, b| hash[a] <=> hash[b] }
# calculating "apple".length
# calculating "pear".length
# calculating "fig".length
#=> ["fig", "pear", "apple"]

After the sort, our hash looks like this:

hash #=> {"apple"=>5, "pear"=>4, "fig"=>3}

Applied to your code, something like this should work:

hash = { |h, k| h[k] = [k.points, k.goal_dif, k.goals] }
teams.sort { |a, b| hash[a] == hash[b] ? a_beats_b(i, j) : hash[a] <=> hash[b] }