I have this Rspec test that is failing and i'm not understanding how to solve it.
it seems like the error is because they are differents instances of the object, so they got differents memory values.
How can i maintain a memory value object when create a object that will behaviour in the same way if given the same input?
describe '#==' do
let(:cpf) {described_class.new('01201201202')}
it 'verifies the key equality' do
expect(cpf).to eq described_class.new('01201201202')
Error:
1) PixKey#== verifies the key equality
Failure/Error: expect(cpf).to eq described_class.new('01201201202')
expected: #<PixKey:0x0000018d191b8670 #value="01201201202", #key="01201201202", #type="cpf">
got: #<PixKey:0x0000018d191b8b70 #value="01201201202", #key="01201201202", #type="cpf">
(compared using ==)
Diff:
## -1,4 +1,4 ##
-#<PixKey:0x0000018d191b8670
+#<PixKey:0x0000018d191b8b70
#key="01201201202",
#type="cpf",
#value="01201201202">
Any ideas would be great.
The default behavior of the == method returns true when both objects refer to the same hash key. Because in your example both elements are different instances their hash value would be different.
If you want two instances of PixKey to be considered equal if they have the same #value and/or #key then you need to override the default implementation with your own implementation, for example like this:
# in your `PixKey` class
def ==(other)
self.class == other.class && #key == other.key && #value == other.value
end
Related
The Ruby docs read as follows:
The eql? method returns true if obj and other refer to the same hash key.
So in order to use #eql? to compare two objects (or use objects as Hash keys), the object has to implement #hash in a meaningful manner.
How come the following happens?
class EqlTest
def hash
123
end
end
a = EqlTest.new
b = EqlTest.new
a.hash == b.hash # => true
a.eql? b # => false
I could of course implement EqlTest#eql? but shouldn't the implementation inherited from Object be something along the lines of hash == other.hash already?
Thanks for your hints!
This seems to be actually the other way around. eql? is expected to return true for objects returning the same hash value, but it is not defined to compare these values. You are simply expected to override both.
The eql? method returns true if obj and other refer to the same hash key. This is used by Hash to test members for equality. For any pair of objects where eql? returns true, the hash value of both objects must be equal. So any subclass that overrides eql? should also override hash appropriately.
I want to be able to find a custom class in my set given just a string. Like so:
require 'set'
Rank = Struct.new(:name, keyword_init: true) {
def hash
name.hash
end
def eql?(other)
hash == other.hash
end
def ==(other)
hash == other.hash
end
}
one = Rank.new(name: "one")
two = Rank.new(name: "two")
set = Set[one, two]
but while one == "one" and one.eql?("one") are both true, set.include?("one") is still false. what am i missing?
thanks!
Set is built upon Hash, and Hash considers two objects the same if:
[...] their hash value is identical and the two objects are eql? to each other.
What you are missing is that eql? isn't necessarily commutative. Making Rank#eql? recognize strings doesn't change the way String#eql? works:
one.eql?('one') #=> true
'one'.eql?(one) #=> false
Therefore it depends on which object is the hash key and which is the argument to include?:
Set['one'].include?(one) #=> true
Set[one].include?('one') #=> false
In order to make two objects a and b interchangeable hash keys, 3 conditions have to be met:
a.hash == b.hash
a.eql?(b) == true
b.eql?(a) == true
But don't try to modify String#eql? – fiddling with Ruby's core classes isn't recommended and monkey-patching probably won't work anyway because Ruby usually calls the C methods directly for performance reasons.
In fact, making both hash and eql? mimic name doesn't seem like a good idea in the first place. It makes the object's identity ambiguous which can lead to very strange behavior and hard to find bugs:
h = { one => 1, 'one' => 1 }
#=> {#<struct Rank name="one">=>1, "one"=>1}
# vs
h = { 'one' => 1, one => 1 }
#=> {"one"=>1}
what am i missing?
What you are missing is that "one" isn't in your set. one is in your set, but "one" isn't.
Therefore, the answer Ruby is giving you is perfectly correct.
All that you have done with your implementation of Rank is that any two ranks with the same name are considered to be the same by a Hash, Set, or Array#uniq. But, a Rank is not the same as a String.
If you want to be able to have a set-like data structure where you can look up things by one of their attributes, you will have to write it yourself.
Something like (untested):
class RankSet < Set
def [](*args)
super(*args.map(&:name))
end
def each
return enum_for(__callee__) unless block_given?
super {|e| yield e.name }
end
end
might get you started.
Or, instead of writing your own set, you can just use the fact that any arbitrary rank with the right name can be used for lookup:
set.include?(Rank.new(name: "one"))
#=> true
# even though it is a *different* `Rank` object
What is the difference between using eq and eql in rspec tests? Is there a difference between:
it "adds the correct information to entries" do
# book = AddressBook.new # => Replaced by line 4
book.add_entry('Ada Lovelace', '010.012.1815', 'augusta.king#lovelace.com')
new_entry = book.entries[0]
expect(new_entry.name).to eq('Ada Lovelace')
expect(new_entry.phone_number).to eq('010.012.1815')
expect(new_entry.email).to eq('augusta.king#lovelace.com')
end
and:
it "adds the correct information to entries" do
# book = AddressBook.new # => Replaced by line 4
book.add_entry('Ada Lovelace', '010.012.1815', 'augusta.king#lovelace.com')
new_entry = book.entries[0]
expect(new_entry.name).to eql('Ada Lovelace')
expect(new_entry.phone_number).to eql('010.012.1815')
expect(new_entry.email).to eql('augusta.king#lovelace.com')
end
There are subtle differences here, based on the type of equality being used in the comparison.
From the Rpsec docs:
Ruby exposes several different methods for handling equality:
a.equal?(b) # object identity - a and b refer to the same object
a.eql?(b) # object equivalence - a and b have the same value
a == b # object equivalence - a and b have the same value with type conversions]
eq uses the == operator for comparison, and eql ignores type conversions.
The differences are subtle.eq is the same as the ruby implementation of ==. On the other hand eql is the same as the ruby implementation of eql?.
eq checks object equivalence and will type cast to convert different object to the same type.
Two objects are equivalent if they are of the same class and have the same value but they are not necessarily the same object in memory.
expect(:my_symbol).to eq(:my_symbol)
# passes, both are identical.
expect('my string').to eq('my string')
# passes, objects are equivalent
expect(5).to eq(5.0)
# passes, Objects are not equivalent but was type cast to same object type.
eql checks object equivalence and does not try type casting.
expect(:my_symbol).to eql(:my_symbol)
# passes, both are identical.
expect('my string').to eql('my string')
# passes, objects are equivalent but not identical
expect(5).to eql(5.0)
# fails, Objects are not equivalence, did not try to type cast
equal checks object identity.
Two object are identical if they are the same object meaning they have same object id (share the same address in memory).
expect(:my_symbol).to equal(:my_symbol)
# passes, both are identical.
expect('my string').to equal('my string')
# fails, objects are equivalent but not identical
expect(5).to equal(5.0)
# fails, objects are not equivalent and not identical
I started working through some sample problems on Test-First, and had worked out a solution which passed all the RSpec tests using Ruby 1.8.7. I just upgraded my OS, and Ruby upgraded as well; my code no longer passes the RSpec test. Can anyone help me understand why this is not working anymore?
My code
def entries
#d
end
the error message
Failures:
1) Dictionary can add whole entries with keyword and definition
Failure/Error: #d.entries.should == {'fish' => 'aquatic animal'}
expected: {"fish"=>"aquatic animal"}
got: {["fish"]=>["aquatic animal"]} (using ==)
Diff:
## -1,2 +1,2 ##
-"fish" => "aquatic animal"
+["fish"] => ["aquatic animal"]
#
I can't figure out what to change about the formatting. (One of the RSpec tests is that the #d must be empty when created, so when I try modifying the #d by putting in explicit formatting it also fails, but I'm imagining that there's a straightforward type issue here I'm not understanding.)
Update: More code
class Dictionary
def initialize d = {}
#d = d
end
def entries
#d
end
def keywords
#d.keys.sort
end
def add words
n_key = words.keys
n_val = words.values
#d[n_key] = n_val
end
end
It looks like you're trying to do some kind of mass assignment by adding several words at once, but that's not the way to do it.
A Ruby Hash can have anything as a key, and this includes arrays of things. It's not like JavaScript where it will automatically cast to string, or other languages that have the same sort of conversion to a specific dictionary key type. In Ruby any object will do.
So your add words method should be:
def add words
words.each do |word, value|
#d[word] = value
end
end
As a note using names like #d is really bad form. Try and be more specific about what that is, or you risk confusing people endlessly. Programs filled with things like #d, x and S are awful to debug and maintain. Better to be clear if a bit verbose than terse and ambiguous.
Secondly, it's not clear how your Dictionary class is all that different from Hash itself. Maybe you could make it a subclass and save yourself some trouble. For example:
class Dictionary < Hash
def keywords
keys.sort
end
def add words
merge!(words)
end
end
In general terms it's always best to use the core Ruby classes to do what you want, then build out from there. Re-inventing the wheel leads to incompatibility and frustration. The built-in Hash class has a whole bunch of utility methods that are very handy for doing data transformation, conversion and iteration, things you're losing by creating your own opaque wrapper class.
The merge! method in particular adds data to an existing Hash, which is exactly what you want.
From what I have understood, the equal method checks if the object is the same.
person = Person.create!(:name => "David")
Person.find_by_name("David").should equal(person)
This should be true.
But aren't there two different objects here?
How could two objects be the same? I don't understand that.
Rails and RSpec equality tests have a variety of choices.
Rails 3.2 ActiveRecord::Base uses the == equality matcher.
It returns true two different ways:
If self is the same exact object as the comparison object
If self is the same type as the comparison object and has the same ID
Note that ActiveRecord::Base has the == method which is aliased as eql?. This is different than typical Ruby objects, which define == and eql? differently.
RSpec 2.0 has these equality matchers in rspec-expectations:
a.should equal(b) # passes if a.equal?(b)
a.should eql(b) # passes if a.eql?(b)
a.should == b # passes if a == b
RSpec also has two equality matchers intended to have more of a DSL feel to them:
a.should be(b) # passes if a.equal?(b)
a.should eq(b) # passes if a == b
In your example you're creating a record then finding it.
So you have two choices for testing #find_by_name:
To test if it retrieves the exact same object OR an equivalent Person record with the same ID, then use should == or its equivalent a.should eql or its DSL version should eq
To test if it uses the exact same object NOT an equivalent Person record with the same ID, then use should equal or its DSL version should be
equal checks if the reference is the same. It corresponds to the Object#equal? method. You want to use == to compare these objects.