Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a pretty syntax for complex comparison?

The <=> method should return -1, 0 or 1 for "less than", "equal to", and "greater than", respectively. For some types of sortable objects, it is normal to base the sort order on multiple properties. The following works, but I think it looks clumsy:

class LeagueStats
  attr_accessor :points, :goal_diff
  def initialize pts, gd
    @points = pts
    @goal_diff = gd
  end
  def <=> other
    compare_pts = points <=> other.points
    return compare_pts unless compare_pts == 0
    goal_diff <=> other.goal_diff
  end
end

Trying it:

[
  LeagueStats.new( 10, 7 ),
  LeagueStats.new( 10, 5 ),
  LeagueStats.new( 9, 6 )
].sort
# => [
#      #<LS @points=9, @goal_diff=6>,
#      #<LS @points=10, @goal_diff=5>,
#      #<LS @points=10, @goal_diff=7>
#    ]

Perl treats 0 as a false value, which allows complex comparisons with different syntax:

{ 
  return ( $self->points <=> $other->points ) ||
         ( $self->goal_diff <=> $other->goal_diff ); 
}

I find the fall-through on 0 via the || operator simple to read and elegant. One thing I like about using || is the short-circuiting of calculations once the comparison has a value.

I cannot find anything similar in Ruby. Are there any nicer ways to build the same complex of comparisons (or anything else picking the first non-zero item), ideally without needing to calculate all values in advance?

like image 901
Neil Slater Avatar asked Aug 29 '13 08:08

Neil Slater


2 Answers

In addition to sawa's answer, the result can also should be evaluated in-place, in order to return -1, 0 or +1:

class LeagueStats
  def <=> other
    [points, goal_diff] <=> [other.points, other.goal_diff]
  end
end

This works because of Array#<=>:

Arrays are compared in an “element-wise” manner; the first two elements that are not equal will determine the return value for the whole comparison.

After implementing <=>, you can include Comparable and get <, <=, ==, >=, > and between? for free.

like image 102
Stefan Avatar answered Sep 29 '22 16:09

Stefan


You can use Enumerable#sort_by instead of sort:

[LeagueStats.new(10,7),LeagueStats.new(10,5),LeagueStats.new(9,6)].sort_by { |ls|
  [ls.points, ls.goal_diff]
}
# => [#<LeagueStats:0x00000001a806c8 @points=9, @goal_diff=6>,
#     #<LeagueStats:0x00000001a806f0 @points=10, @goal_diff=5>,
#     #<LeagueStats:0x00000001a80718 @points=10, @goal_diff=7>]

class LeagueStats
  ...

  def to_a
    [points, goal_diff]
  end

  def <=> other
    to_a <=> other.to_a
  end
end
like image 39
falsetru Avatar answered Sep 29 '22 16:09

falsetru