user10111321
user10111321

Reputation:

Why do some elements remain even if the condition matches for each one?

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

Answers (2)

ray
ray

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

DonPaulie
DonPaulie

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

Related Questions