On this page:
https://rubymonk.com/learning/books/4-ruby-primer-ascent/chapters/45-more-classes/lessons/105-equality_of_objects
I am trying to correct this code, so that it passes its tests.
My attempt is quite bad as I am only just beginning to learn how software logic works.
class Item
attr_reader :item_name, :qty
def initialize(item_name, qty)
#item_name = item_name
#qty = qty
end
def to_s
"Item (#{#item_name}, #{#qty})"
end
def ==(other_item)
end
end
p Item.new("abcd",1) == Item.new("abcd",1)
p Item.new("abcd",2) == Item.new("abcd",1)
This is my solution, but it is not correct:
class Item
attr_reader :item_name, :qty
def initialize(item_name, qty)
#item_name = item_name
#qty = qty
end
def to_s
"Item (#{#item_name}, #{#qty})"
end
def ==(other_item)
if self == other_item
return true
else
return false
end
end
end
p Item.new("abcd",1) == Item.new("abcd",1)
p Item.new("abcd",2) == Item.new("abcd",1)
I was hoping a Rubyist out there could help me solve this exercise. I am unsure of how to solve it.
Thanks for your help
here is the output from the test:
STDOUT:
nil
nil
Items with same item name and quantity should be equal
RSpec::Expectations::ExpectationNotMetError
expected Item (abcd, 1) got Item (abcd, 1) (compared using ==) Diff:
Items with same item name but different quantity should not be equal ✔
Items with same same quantity but different item name should not be equal ✔
When you override the == method, you should give meaning to your comparison. The default == behavior checks that the other item is identical to the compared item (they have the same object_id). Try this:
def ==(other_item)
other_item.is_a?(Item) &&
self.item_name == other_item.item_name &&
self.qty == other_item.qty
end
Can point you in right direction instead of telling the answer.
You are comparing the references of objects for equality, whereas, you are asked to compare only those attributes for equality. That is, compare both objects parameters in such a way that if they are equal, it must return true; else false
When you scroll down past the question you will see that the next example provides a clear solution.
def ==(other_item)
self.item_name == other_item.item_name &&
self.qty == other_item.qty
end
end
Related
Write a simple DSL for creating a shopping list. We should be able to specify the item name and quantity..
Something like.
My code works for pre-defined hash map, but once I take user input for creating a hash map it fails and also I want to improve my code by using more advanced concepts to achieve this result. Any suggestions ?
class Store
def initialize(item, quantity)
#products = { "item" => quantity }
#cart = []
end
def add_to_cart( item )
#cart << item
end
def add_product( item, price )
#products[item] = price
end
def cart_total
#cart.inject(0){|sum, item| sum + #products[item]}
end
def items
#products.join(', ')
end
end
puts "Please provide item name"
item = gets.chomp
puts "Please provide quantity associated with item"
quantity = gets.chomp.to_i
store = Store.new(item, quantity)
store.add_to_cart(item)
puts store.cart
printf "$%6.2f", store.cart_total
Expected Result:
s1.list #Should print complete list of the item and values added
sl.total # Should list the total price value for shopping done.
It's a quite broad question and it shouldn't, but I'd like to give my view on this anyway.
Initialize empty Hash with default values (Hash#new)
Don't forget attr_reader for accessing instance variable (Module#attr_reader)
Check the docs for the method used here (Hash, Array, Enumerable)
Here is a possible refactor:
class Store
attr_reader :cart, :products # you need to access the variables
def initialize
#cart = Hash.new(0) # initilize the Hash with default value
#products = Hash.new
end
def add_to_cart(item, quantity)
# increase by quantity, thanks to default value of the Hash,
# only if the product is available
if products.has_key? item # products thanks to the attr_reader, then see Hash docs
cart[item] += quantity
true
else
false
end
end
def add_product(item, price)
products[item] = price
end
def cart_total
cart.sum { |k, v| products[k] * v }
end
def products_list
products.keys
end
end
So, you can use this class as follows:
store = Store.new
store.add_product("Apple", 10.0 )
store.add_product("Lemon", 12.0 )
store.add_product("Melon", 20.0 )
store.products_list
#=> ["Apple", "Lemon", "Melon"]
store.products
#=> {"Apple"=>10.0, "Lemon"=>12.0, "Melon"=>20.0}
store.add_to_cart('Apple', 4) #=> true
store.add_to_cart('Melon', 5) #=> true
store.add_to_cart('Carrot', 1) #=> false # not available, not added
store.cart # thanks to attr_reader
#=> {"Apple"=>4, "Melon"=>2}
store.cart_total
#=> 140.0
You could think also to define a Cart class...
I am doing a command line of game of Rock Paper Scissors Lizard Spock in Ruby. I have the matchup method that takes the variables #shape (the hand shape randomly selected by the game) and #player_shape (the hand shape chosen by the player).
My matchup method compares the two shapes and sets the game result to #result:
class Game
SHAPES = [:rock, :paper, :scissors, :lizard, :spock]
attr_reader :shape, :player_shape, :status
# ...
def matchup
if #shape == #player_shape
#result = :draw
else
case #player_shape
when :rock
#result = (#shape == :scissors || #shape == :lizard) ? :player_wins : :game_wins
when :paper
#result = (#shape == :rock || #shape == :spock) ? :player_wins : :game_wins
when :scissors
#result = (#shape == :paper || #shape == :lizard) ? :player_wins : :game_wins
when :lizard
#result = (#shape == :paper || #shape == :spock) ? :player_wins : :game_wins
when :spock
#result = (#shape == :rock || #shape == :scissors) ? :player_wins : :game_wins
end
end
end
# ...
end
I ran the code many times and it works as expected, but in my spec file, the result I'm getting is not matching the code behavior. Here is the spec:
describe Game do
subject(:game) { Game.new }
# ...
describe "#matchup" do
context "game chooses rock" do
before do
allow(game).to receive(:shape).and_return(:rock)
end
it "sets result as :player_wins if the game chooses paper" do
allow(game).to receive(:player_shape).and_return(:paper)
game.matchup
expect(game.result).to eq(:player_wins)
end
end
end
end
and here is the result:
Failure/Error: expect(game.result).to eq(:player_wins)
expected: :player_wins
got: :draw
(compared using ==)
Diff:
## -1,2 +1,2 ##
-:player_wins
+:draw
What I am doing wrong here? I've tried and tried and still can't figure out how to solve this problem.
Here
allow(game).to receive(:shape).and_return(:rock)
and there
allow(game).to receive(:player_shape).and_return(:paper)
You're stubbing methods Game#shape and Game#player_shape for game object.
But you're using #shape == #player_shape as condition in if statement.
Ruby treats undefined instance variables (those which starts from #) as nil by default.
→ irb
irb(main):001:0> #shape
=> nil
Thus #shape == #player_shape is the same as nil == nil in your case. Which is actually a true. What's why program flow reach #result = :draw line.
To make it work you need to define shape and player_shape methods by using attr_reader, for example, and use them instead of instance variables.
upd
Now replace #shape with shape and #player_shape with player_shape in your code. Be sure you initialise values for them.
Take a look at that Ruby Monk tutorial (or any other articles) about instance vairables to understand how they work
Don't understand why #nums.pop won't work in the value method. It seems to tell me that it can't do that for nil, but if I just say #nums, it shows that there is indeed something in the array. So then why can't I pop it out?
class RPNCalculator
def initialize
#value = value
nums ||= []
#nums = nums
end
def push(num)
#nums << num
end
def plus
if #nums[-2] == nil || #nums[-1] == nil
raise "calculator is empty"
else
#value = #nums.pop + #nums.pop
#nums.push(#value)
end
end
def minus
if #nums[-2] == nil || #nums[-1] == nil
raise "calculator is empty"
else
#value = #nums[-2] - #nums[-1]
#nums.pop(2)
#nums.push(#value)
end
end
def divide
if #nums[-2] == nil || #nums[-1] == nil
raise "calculator is empty"
else
#value = #nums[-2].to_f / #nums[-1].to_f
#nums.pop(2)
#nums.push(#value)
end
end
def times
if #nums[-2] == nil || #nums[-1] == nil
raise "calculator is empty"
else
#value = #nums.pop.to_f * #nums.pop.to_f
#nums.push(#value)
end
end
def value
#nums #Don't understand why #nums.pop won't work here
end
def tokens(str)
str.split(" ").map { |char| (char.match(/\d/) ? char.to_i : char.to_sym)}
end
def evaluate(str)
tokens(str).each do |x|
if x == ":-"
minus
elsif x == ":+"
plus
elsif x == ":/"
divide
elsif x ==":*"
times
else
push(x)
end
end
value
end
end
Error relates to the following part of a spec:
it "adds two numbers" do
calculator.push(2)
calculator.push(3)
calculator.plus
calculator.value.should == 5
end
Error says either:
Failure/Error: calculator.value.should == 5
expected: 5
got: [5] <using ==>
OR if .pop is used
Failure/Error: #calculator = RPNCalculator.new
NoMethodError:
undefined method 'pop' for nil:NilClass
Your initialize method assigning #value = value calls the function at def value which returns #nums which has not yet been created in initialize since #nums is created afterwards with nums ||= []; #nums = nums therefore it's nil. This is why .pop won't work.
You've created #nums as an array with nums ||= [] and you're using it with push and pop so why are you checking for the value with value.should == 5 (Integer) when calling value returns an (Array). You would need to write it like value.first.should == 5 or value[0].should == 5 ... otherwise you should change value to return just the element you want
def value
#nums.pop # or #nums[0], or #nums.first or #nums.last however you plan on using it
end
The problem is #value = value in your initialize method. Fix that then you can add the .pop in value.
EDIT
Also your evaluation is calling methods before you've populated #nums with the values. Then the methods "raise" errors. You can't call minus after only one value has been pushed to #nums.
Here's how I would do the flow for splitting the string
# Multiplication and Division need to happen before addition and subtraction
mylist = "1+3*7".split(/([+|-])/)
=> ["1", "+", "3*7"]
# Compute multiplication and division
mylist = mylist.map {|x| !!(x =~ /[*|\/]/) ? eval(x) : x }
=> ["1", "+", 21]
# Do the rest of the addition
eval mylist.join
=> 22
I realize this isn't exactly how you're going about solving this... but I think splitting by order of mathematical sequence will be the right way to go. So first evaluate everything between (), then only multiplication and division, then all addition and subtraction.
EDIT I just looked into what a RPN Calculator is. So don't mind my splitting recommendation as it doesn't apply.
I use in a genealogy program following method to add a mariage/spouse to a Person.
#mariages is an array of arrays.
def add_spouse(spouse, mariage_date = nil, divorce_date = nil)
#mariages.push([spouse, mariage_date, divorce_date]) unless #mariages.index{|(a, b, c)| a == spouse && b == mariage_date}
spouse.mariages.push(self) unless spouse.mariages.index{|(a, b, c)| a == self && b == mariage_date}
end
With the unless #mariages.index{|(a, b, c)| a == spouse && b == mariage_date} i check if the mariage is not allready in the array.
Now i want to keep my mariages in an array of hashes like this
def add_spouse(spouse, mariage_date = nil, divorce_date = nil)
#mariages.push({:spouse => spouse, :mariage_date => mariage_date, :divorce_date => divorce_date}) unless ...
spouse.mariages.push({:spouse => self, :mariage_date => mariage_date, :divorce_date => divorce_date}) unless ...
end
Can someone help me adapt the unless part to do the check if the hash is not allready presant in the array ?
In the block that goes into index the element getting iterated over is a hash so you should use
.. unless #mariages.index{|h| h[:spouse] == spouse && h[:mariage_date] == mariage_date}
and
.. unless spouse.mariages.index{|h| h[:spouse] == self && h[:mariage_date] == mariage_date}
PS: mariage is misspelled. It should be marriage.
Since your array now contains hashes instead of other arrays, you can't use "array unpacking" (not sure what's the official term for that is). You will get an instance of hash, and you can access it as you normally would.
#mariages.push({:spouse => spouse,
:mariage_date => mariage_date,
:divorce_date => divorce_date}) unless #mariages.index{|h| h[:spouse] == spouse && h[:mariage_date] == mariage_date}
As per your question in the commend I'd do something like the following. Note that Dates should use the date class and the partners in the marriage should be classes too, with births and deaths and whatnot. But hopefully you can see how moving the data into objects that know what to do with it, can simplify the design as things get larger. (I also went with bride and groom for simplicity, feel free to change that in a genealogically approved manner).
Each person would have a Marriages associated with them and if you had a bride and a groom they could share a single Marriage, but have a different list of Marriages.
class Marriage
attr_accessor :marriage_date, :divorce_date, :bride, :groom
def initialize(date, bride, groom)
#marriage_date = date
#bride = bride
#groom = groom
end
def marriage_equals(m)
return (#marriage_date == m.marriage_date) &&
(#bride == m.bride) &&
(#groom == m.groom)
end
end
class Marriages
def initialize
#marriages = []
end
def add_marriage(marriage)
if (#marriages.any? { |m| m.marriage_equals(marriage) })
puts "Marriage of #{marriage.groom} already listed"
return false
else
puts "Added new marriage"
#marriages.push(marriage)
return true
end
end
end
m1 = Marriage.new("1-1-0002", "Wilma", "Fred")
m2 = Marriage.new("6-8-0003", "Betty", "Barney")
m3 = Marriage.new("2-8-8003", "Jane", "George")
marriages = [m1,m2]
p marriages.any? { |m| m.marriage_equals(m1) } # true
p marriages.any? { |m| m.marriage_equals(m3) } # false
m_list = Marriages.new
m_list.add_marriage(m1) # Added new marriage
m_list.add_marriage(m2) # Added new marriage
m_list.add_marriage(m2) # Marriage of Barney already listed
I have a few arrays of Ruby objects of class UserInfo:
class UserInfo
attr_accessor :name, :title, :age
end
How can I merge these arrays into one array? A user is identified by its name, so I want no duplicate names. If name, title, age, etc. are equal I'd like to have 1 entry in the new array. If names are the same, but any of the other details differ I probably want those 2 users in a different array to manually fix the errors.
Thanks in advance
Redefine equality comparison on your object, and you can get rid of actual duplicates quickly with Array#uniq
class UserInfo
attr_accessor :name, :title, :age
def == other
name==other.name and title==other.title and age==other.age
end
end
# assuming a and b are arrays of UserInfo objects
c = a | b
# c will only contain one of each UserInfo
Then you can sort by name and look for name-only duplicates
d = c.sort{ |p,q| p.name <=> q.name } #sort by name
name = ""
e = []
d.each do |item|
if item.name == name
e[-1] = [e[-1],item].flatten
else
e << item
end
end
A year ago I monkey patched a kind of cryptic instance_variables_compare on Object. I guess you could use that.
class Object
def instance_variables_compare(o)
Hash[*self.instance_variables.map {|v|
self.instance_variable_get(v)!=o.instance_variable_get(v) ?
[v,o.instance_variable_get(v)] : []}.flatten]
end
end
A cheesy example
require 'Date'
class Cheese
attr_accessor :name, :weight, :expire_date
def initialize(name, weight, expire_date)
#name, #weight, #expire_date = name, weight, expire_date
end
end
stilton=Cheese.new('Stilton', 250, Date.parse("2010-12-02"))
gorgonzola=Cheese.new('Gorgonzola', 250, Date.parse("2010-12-17"))
irb is my weapon of choice
>> stilton.instance_variables_compare(gorgonzola)
=> {"#name"=>"Gorgonzola", "#expire_date"=>#<Date: 4910305/2,0,2299161>}
>> gorgonzola.instance_variables_compare(stilton)
=> {"#name"=>"Stilton", "#expire_date"=>#<Date: 4910275/2,0,2299161>}
>> stilton.expire_date=gorgonzola.expire_date
=> #<Date: 4910305/2,0,2299161>
>> stilton.instance_variables_compare(gorgonzola)
=> {"#name"=>"Gorgonzola"}
>> stilton.instance_variables_compare(stilton)
=> {}
As you can see the instance_variables_compare returns an empty Hash if the two objects has the same content.
An array of cheese
stilton2=Cheese.new('Stilton', 210, Date.parse("2010-12-02"))
gorgonzola2=Cheese.new('Gorgonzola', 250, Date.parse("2010-12-17"))
arr=[]<<stilton<<stilton2<<gorgonzola<<gorgonzola2
One hash without problems and one with
h={}
problems=Hash.new([])
arr.each {|c|
if h.has_key?(c.name)
if problems.has_key?(c.name)
problems[c.name]=problems[c.name]<<c
elsif h[c.name].instance_variables_compare(c) != {}
problems[c.name]=problems[c.name]<<c<<h[c.name]
h.delete(c.name)
end
else
h[c.name]=c
end
}
Now the Hash h contains the objects without merging problems and the problems hash contains those that has instance variables that differs.
>> h
=> {"Gorgonzola"=>#<Cheese:0xb375e8 #name="Gorgonzola", #weight=250, #expire_date=#<Date: 2010-12-17 (4911095/2,0,2299161)>>}
>> problems
=> {"Stilton"=>[#<Cheese:0xf54c30 #name="Stilton", #weight=210, #expire_date=#<Date: 2010-12-02 (4911065/2,0,2299161)>>, #<Cheese:0xfdeca8 #name="Stilton", #weight=250,#expire_date=#<Date: 2010-12-02 (4911065/2,0,2299161)>>]}
As far as I can see you will not have to modify this code at all to support an array of UserInfo objects.
It would most probably be much faster to compare the properties directly or with a override of ==. This is how you override ==
def ==(other)
return self.weight == other.weight && self.expire_date == other.expire_date
end
and the loop changes into this
arr.each {|c|
if h.has_key?(c.name)
if problems.has_key?(c.name)
problems[c.name]=problems[c.name]<<c
elsif h[c.name] != c
problems[c.name]=problems[c.name]<<c<<h[c.name]
h.delete(c.name)
end
else
h[c.name]=c
end
}
Finally you might want to convert the Hash back to an Array
result = h.values
Here's another potential way. If you have a way of identifying each UserInfo, say a to_str method that prints out the values:
def to_str()
return "#{#name}:#{#title}:#{#age}"
end
You can use inject and a hash
all_users = a + b # collection of users to "merge"
res = all_users.inject({})do |h,v|
h[v.to_str] = v #save the value indexed on the string output
h # return h for the next iteration
end
merged = res.values #the unique users