Reputation:
I have created a small store application in Ruby. The concept is simple: I want to be able to add, remove, and view the 'database' in this example. The code seems to work; however, it does not behave like one would expect. I can add any product just fine, but when I try to remove them by a certain criterion that matches each item, only some are removed.
class Store
PRODUCT = Struct.new(:identifier, :parameters)
def initialize(database = [])
@database = database
end
def increment
# Increment the unique identifier if the database is not empty
# Otherwise, set the initial value for the unique identifier
@database.any? ? @database.map { |object| object[:identifier] }.max + 1 : 1
end
def add(product)
@database.push(identifier: increment, parameters: product)
end
def remove(product)
@database.each do |object|
product.each do |key, value|
@database.delete(object) if object[key] == value
@database.delete(object) if object[:parameters][key] == value
end
end
end
def view
puts @database
puts "\n"
end
end
store = Store.new
store.add(name: 'Fanta', volume: 33, unit: 'centiliter', count: 48, price: 9.95, currency: 'SEK')
store.add(name: 'Sprite', volume: 33, unit: 'centiliter', count: 48, price: 9.95, currency: 'SEK')
store.add(name: 'Coca-Cola', volume: 33, unit: 'centiliter', count: 48, price: 9.95, currency: 'SEK')
store.view
store.remove(price: 9.95)
store.view
This example will add three arbitrary items to the Store
instance and list the contents found in the @database
array. This array will of course hold the values:
{:identifier=>1, :parameters=>{:name=>"Fanta", :volume=>33, :unit=>"centiliter", :count=>48, :price=>9.95, :currency=>"SEK"}}
{:identifier=>2, :parameters=>{:name=>"Sprite", :volume=>33, :unit=>"centiliter", :count=>48, :price=>9.95, :currency=>"SEK"}}
{:identifier=>3, :parameters=>{:name=>"Coca-Cola", :volume=>33, :unit=>"centiliter", :count=>48, :price=>9.95, :currency=>"SEK"}}
The code then proceeds to remove any product with a price of 9.95, which should match all the products in the @database
array. However, when I list its contents once more, one product remains:
{:identifier=>2, :parameters=>{:name=>"Sprite", :volume=>33, :unit=>"centiliter", :count=>48, :price=>9.95, :currency=>"SEK"}}
I cannot find any logic to this. Why does my remove
method not remove all the elements if the condition holds true for each comparison?
Upvotes: 3
Views: 53
Reputation: 5552
Let's focus on following looping,
@database.each do |object|
product.each do |key, value|
puts '-----------------'
@database.delete(object) if object[key] == value
@database.delete(object) if object[:parameters][key] == value
end
end
It is assumed to run 3 iteration on each loop for @database
,
first iteration - it delete first with identifier = 1 for each loop counter 0
second iteration - it get modified @database
as first get deleted, second become first and 3rd become 2nd and counter of each loop is at 1. So it delete new 2nd element of @database
having identifier = 3.
third iteration - It will have each loop counter at 2 and new updated @database
will have only 1 element having identifier = 2 (rescued in 2nd iteration) and there is no third element to proceed iteration further so nothing is iterated.
Correction needed:
def remove(product)
@database.reject! do |object|
product.map do |k,v|
object[k] == v if object.has_key?(k)
object[:parameters][k] == v if object[:parameters].has_key?(k)
end.inject(&:|)
end
end
Upvotes: 1
Reputation: 2214
I think that this problem comes from float comparison. There are many ways to approach this. One of them:
def remove(product)
@database.delete_if do |object|
product.any? do |key, value|
matches?(object[key], value) || matches?(object[:parameters][key], value)
end
end
end
def matches?(obj1, obj2)
if obj1.is_a?(Float) || obj2.is_a?(Float)
(obj1-obj2).abs < 0.001
else
obj1 == obj2
end
end
Upvotes: 0