4

Orginal Question

This is a really horrible method, which checks for equality on base of the code but case agnostic

def ==(another_country)
   (code.nil? ? nil : code.downcase) == (another_country.code.nil? ? nil : another_country.code.downcase) unless another_country.nil?
end

Can you point my in the right direction how to write this more elegant w/o reliying on ugly if else structures?

This is the solution I ended up using (+RSpecs)

# Country model
class Country < ActiveRecord::Base
  attr_accessible :code

  def ==(another_country)
    code.to_s.downcase == another_country.code.to_s.downcase rescue false
  end
end

Extensive Tests:

# RSpec
describe Country do
   describe 'equality based solely on Country.code' do
      before do
        @country_code_de = FactoryGirl.build(:country, :code => 'de')
      end

      it 'should be equal if Country.code is equal' do
        other_country_code_de = FactoryGirl.build(:country, :code => 'de')
        @country_code_de.should == other_country_code_de
      end

      it 'should be not equal if Country.code is not equal' do
        country_code_usa = FactoryGirl.build(:country, :code => 'usa')
        @country_code_de.should_not == country_code_usa
      end

      it 'should be case insensitive' do
        country_code_de_uppercase = FactoryGirl.build(:country, :code => 'DE')
        @country_code_de.should == country_code_de_uppercase
      end

      it 'should not rely on id for equality' do
        @country_code_de.id = 0
        country_code_usa = FactoryGirl.build(:country, :code => 'usa', :id => 0)
        @country_code_de.should_not == country_code_usa
      end

      it 'should be not equal if Country.code of one Country is nil' do
        country_code_nil = FactoryGirl.build(:country, :code => nil)
        @country_code_de.should_not == country_code_nil
      end

      it 'should be equal if Country.code for both countries is nil' do
        country_code_nil = FactoryGirl.build(:country, :code => nil)
        other_country_code_nil = FactoryGirl.build(:country, :code => nil)
        country_code_nil.should == other_country_code_nil
      end

      it 'should be not equal if other Country is nil' do
        @country_code_de.should_not == nil
      end

      it 'should be not equal if other object is not a Country' do
        @country_code_de.should_not == 'test'
      end

      it 'should be equal for descendants of Country with same Country.code' do
        class CountryChild < Country
        end
        country_child = CountryChild.new(:code => 'de')
        @country_code_de.should == country_child
      end
    end
end
wintersolutions
  • 5,173
  • 5
  • 30
  • 51
  • @Visitor: This was my single most productive question on SC to date. The solution in my question is the shortest we could come up with, but there's alot of wisdom to be found in the answers ;) – wintersolutions Feb 19 '12 at 15:44

7 Answers7

1

If you are using Rails:

def ==(another_country)
  return nil unless another_country
  code.try(:downcase) == another_country.code.try(:downcase)
end
iltempo
  • 15,718
  • 8
  • 61
  • 72
1

nil has a to_s method:

def ==(another_country)
   #return nil if another_country.nil?
   self.code.to_s.downcase == another_country.code.to_s.downcase
end
steenslag
  • 79,051
  • 16
  • 138
  • 171
1

Perhaps you could break the logic into two methods, one returning the object's identity, another for checking equality:

class MyClass
  def identity
    return nil if code.nil?
    code.downcase
  end

  def ==(other)
    return false unless other.is_a?(MyClass)
    self.identity == other.identity
  end
end
Mladen Jablanović
  • 43,461
  • 10
  • 90
  • 113
1

Since any value that is not nil or false acting like true in conditions, there is some tricks what you can do with the code.

The expression like

(code.nil? ? nil : code.downcase)

can be painlessly replaced by

(code.downcase if code) # or by this one (code && code.downcase)

The second one

(do_something) unless another_country.nil?

as same as

(do_something) if another_country 
# or 
another_contry && (do_something)

So eventually you can turn your method into this

def ==(another_country)
  code && another_country.code && 
  code.downcase == another_country.code.downcase
end

Some tests

class Country
  attr_accessor :code

  def initialize(code)
    @code = code
  end

  def ==(another_country)
    code && another_country.code &&
    code.downcase == another_country.code.downcase
  end
end

p Country.new("FOObar") == Country.new("fooBAR") # => true
p Country.new(nil)      == Country.new(nil)      # => nil
p Country.new("XXX")    == Country.new(nil)      # => nil
p Country.new(nil)      == Country.new("XXX")    # => nil
evfwcqcg
  • 15,755
  • 15
  • 55
  • 72
  • Your solution doesn't work if both codes are `nil`. I learned alot from your code and I'm sure your approach of breaking a big problem down into small subproblems will come handy for me in the future (+1) – wintersolutions Feb 19 '12 at 15:38
1

How about this,

def ==(another_country)
   return false if code.blank? # Remove this line if you want to return true if code and antoher_country.code are nil
   code.to_s.downcase == another_country.to_s.code.downcase rescue false
end

Here if any of code, another_country or another_country.code is nil, it will through up an exception and rescue false statement will return false value.

If everything goes well, the comparison will happen and true or false will be returned based on the input.

nkm
  • 5,844
  • 2
  • 24
  • 38
0
def == (another_country)
  return unless another_country.is_a?(Country)
  return if code.nil? || another_country.code.nil?

  code.casecmp(another_country.code).zero?
end

The class check is a good practice in case you end up with an array of mixed types.

If you are not worried about the '' vs nil case you can compress it a bit to the following. I don't think it's worth it though.

def == (another_country)
  code.try(:casecmp, another_country.code.to_s).try(:zero?) if another_country.is_a?(Country)
end

Note, if you are overriding == you should also override eql? and hash otherwise you can get unexpected results, with hashes and enumerable methods.

Ruby Monk - Equality of Objects

Joshua
  • 2,079
  • 20
  • 29
0
def == (another_country)    
    if code.nil? || another_country.nil? || another_country.code.nil?
      return nil
    end

    code.downcase == another_country.code.downcase
end

This way, it is visible at a glance what you are doing - nil check and a comparision.

Femaref
  • 60,705
  • 7
  • 138
  • 176
  • I think you're missing a `return`. Also, what's the point of returning `nil` from `==` method? I believe it should always return a Boolean. – Mladen Jablanović Feb 19 '12 at 13:57
  • I'm just mirroring his original expression - it has an unless at the end. `something unless true` results in `nil`. The `if` part is missing a return, yes. – Femaref Feb 19 '12 at 15:50