I have a complex multi nested array of hashes like below:
{
"Food":[
{
"id": "01",
"name":"ABC",
"branch":"London",
"platter_cost":"£40.00",
"Kebab":[
{
"name":"chicken",
"value":"£8.12"
},
{
"name":"lamb",
"value":"£9.67"
}
],
"sides":[
{
"type":"drinks",
"options":[
{
"id":1,
"name":"Coke",
"price":"£4.70"
},
{
"id":2,
"name":"Pepsi",
"price":"£2.90"
},
{
"id":3,
"name":"Tango",
"price":"£4.00"
}
]
},
{
"type":"chips",
"options":[
{
"id":4,
"name":"Peri-Peri",
"price":"£4.00"
}
]
}
]
},
{
"id": "02",
"name":"XYZ",
"branch":"Manchester",
"platter_cost":"£30.00",
"Kebab":[
{
"name":"chicken",
"value":"£5.22"
},
{
"name":"lamb",
"value":"£6.35"
}
],
"sides":[
{
"type":"drinks",
"options":[
{
"id":77,
"name":"coke",
"price":"£3.70"
},
{
"id":51,
"name":"Orange",
"price":"£4.00"
},
{
"id":33,
"name":"Apple",
"price":"£2.00"
}
]
},
{
"type":"chips",
"options":[
{
"id":20,
"name":"peri-peri",
"price":"£4.00"
},
{
"id":18,
"name":"cheesy",
"price":"£3.50"
}
]
}
]
}
]
}
I have a method to return a cost value based on the arguments. Example:
def total_cost(id: "01", options: [1, 4], kebab: 'chicken')
platter_cost + (sum of)options + cost of chicken kebab
end
Arguments explanation:
First argument: id is the main id(company_id),
Second argument: options: [1, 4]. 1 and 4 are the id's inside the Side options, The ids are unique so it doesn't matter the options are chips or drinks.
Third argument: is the cost of the chicken kebab.
So the output for the id: "01" is £16.82. coke_cost + tango_cost + chicken_kebab_cost
what is the clean and efficient way to get the results?
So far I tried the below but am a bit lost on which way to choose. Thanks in advance.
def dishes
file = File.read('food.json')
obj = JSON.parse(file)
obj['food']
end
def self_hash # Trying to create a single hash like an active record object
h = {}
dishes.each do |dish|
h["id"] = dish["id"]
h["platter_cost"] = dish["platter_cost"]
h["kebab"] = dish["kebab"].each{ |k| {"chicken: #{k["chicken"]}", "lamb: #{k["lamb"]}"} } # Not working
end
end
This is an awkward data structure to work with. It's unfortunate it can't be changed, but we can do things to make it easier to work with.
First, turn it into a class so we have something to hang behavior off of.
class Dishes
attr_reader :dishes
def initialize(dishes)
#dishes = dishes
end
Now we need to get the right pieces of dishes. Unfortunately dishes is poorly designed. We can't just do dishes[id] we need to search through Arrays for matches. With a class we can write methods to abstract away working with this awkward data structure.
Let's abstract away having to dig into the Food key every time.
def menus
#dishes.fetch(:Food)
end
Note that it's the Symbol :Food, not the string "Food". "Food":[...] produces a Symbol.
Note that I'm using fetch because unlike [] it will throw a KeyError if Food is not found. This makes error handling much easier. I'll be using fetch consistently through the code.
Also note that the method is called menus because this appears to be a better description of what dishes["Food"] is: a list of menus for various locations.
Now we can search menus for a matching id using Enumerable#find. Again, we abstract this away in a method.
def menu(id)
menu = menus.find { |m| m.fetch(:id) == id }
raise "Can't find menu id #{id}" if !menu
return Menu.new(menu)
end
Not only is finding a menu abstracted away, but we also have proper error handling if we can't find it.
Now that we've found the menu we want, we can ignore the rest of the data structure. We have a Menu class just for working with the menu.
class Menu
attr_reader :menu
def initialize(menu)
#menu = menu
end
We can now fetch the kebabs. Searching an Array is awkward. Let's turn it into a more useful Hash keyed on the name of the kebab.
# Turn the list of kebabs into a hash keyed on
# the name. Cache the result.
def kebabs
#kebabs ||= menu.fetch(:Kebab).each_with_object({}) { |k,h|
h[ k[:name] ] = k
}
end
Now we can search the Hash of kebabs for matching names using Hash#fetch_values. Note it's names because someone might want to order more than one delicious kebab.
def find_kebabs(names = [])
kebabs.fetch_values(*names)
end
An advantage of this approach is we'll get a KeyError if a kebab does not exist.
Like with the kebabs, we want to turn all the sides into one hash keyed on the ID. Getting all the sides is a bit tricky. They're broken up into several different Arrays. We can use flat_map to flatten the sides into one Array.
def sides
# Flatten out the list of sides into one Array.
# Then turn it into a Hash keyed on the ID
#sides ||= menu.fetch(:sides).flat_map { |types|
types.fetch(:options)
}.each_with_object({}) { |s,h|
h[ s[:id] ] = s
}
end
Now that it's flattened we can search the Hash just like we did with kebabs.
def find_sides(ids = [])
sides.fetch_values(*ids)
end
Now that we have these methods we can find the sides and kebabs. Again, the data structure is working against us. The price is in a string with a £. If we want to total up the prices we need to turn "£4.00" into 4.00
def price_to_f(price)
price.gsub(/^\D*/, '').to_f
end
And where the price is stored is inconsistent. For kebabs it's value and for sides its price. More methods to smooth this over.
def side_price(side)
price_to_f(side.fetch(:price))
end
def kebab_price(kebab)
price_to_f(kebab.fetch(:value))
end
(Note: Kebab and Side could be their own classes with their own price methods)
Finally we can put it all together. Find the items and sum their prices.
def price(kebabs:[], sides:[])
price = find_kebabs(kebabs).sum { |k| kebab_price(k) }
price += find_sides(sides).sum { |s| side_price(s) }
return price
end
It would look like so.
dishes = Dishes.new(data)
menu = dishes.menu("01")
p menu.price(kebabs: ["chicken"], sides: [1,3])
If any kebabs or sides are not found you get a KeyError.
menu.price(kebabs: ["chicken"], sides: [1,398,3])
test.rb:149:in `fetch_values': key not found: 398 (KeyError)
We can make the error handling a bit more robust by writing up some custom KeyError exceptions.
class Menu
class SideNotFoundError < KeyError
def message
#message ||= "Side not found: #{key}"
end
end
class KebabNotFoundError < KeyError
def message
#message ||= "Kebab not found: #{key}"
end
end
end
Then we can modify our finder methods to throw these exceptions instead of a generic KeyError.
def find_sides(ids = [])
sides.fetch_values(*ids)
rescue KeyError => e
raise SideNotFoundError, key: e.key
end
def find_kebabs(names = [])
kebabs.fetch_values(*names)
rescue KeyError => e
raise KebabNotFoundError, key: e.key
end
These more specific errors allow for more robust error handling while maintaining the Menu black box.
begin
price = menu.price(kebabs: ["chicken"], sides: [1,398,3])
# more code that depends on having a price
rescue Menu::KebabNotFoundError => e
# do something when a kabab is not found
rescue Menu::SideNotFoundError => e
# do something when a side is not found
end
This might seem like overkill, I'm sure someone can come up with some clever compressed code. It's worth it. I work with awkward and inconsistent data structures all the time; a class makes working with them much easier in the long run.
It breaks the problem down into small pieces. These pieces can then be unit tested, documented, given robust error handling, and used to build more functionality.
Here it is all spelled out.
class Dishes
attr_reader :dishes
def initialize(dishes)
#dishes = dishes
end
def menus
dishes.fetch(:Food)
end
def menu(id)
menu = menus.find { |m| m[:id] == id }
raise "Can't find menu id #{id}" if !menu
return Menu.new(menu)
end
end
class Menu
attr_reader :menu
def initialize(menu)
#menu = menu
end
def sides
# Flatten out the list of sides and turn it into
# a Hash keyed on the ID.
#sides ||= menu.fetch(:sides).flat_map { |types|
types.fetch(:options)
}.each_with_object({}) { |s,h|
h[ s[:id] ] = s
}
end
# Turn the list of kebabs into a hash keyed on
# the name.
def kebabs
#kebabs ||= menu.fetch(:Kebab).each_with_object({}) { |k,h|
h[ k[:name] ] = k
}
end
def find_sides(ids = [])
sides.fetch_values(*ids)
rescue KeyError => e
raise SideNotFoundError, key: e.key
end
def find_kebabs(names = [])
kebabs.fetch_values(*names)
rescue KeyError => e
raise KebabNotFoundError, key: e.key
end
def price_to_f(price)
price.gsub(/^\D*/, '').to_f
end
def side_price(side)
price_to_f(side.fetch(:price))
end
def kebab_price(kebab)
price_to_f(kebab.fetch(:value))
end
def price(kebabs:[], sides:[])
price = find_kebabs(kebabs).sum { |k| kebab_price(k) }
price += find_sides(sides).sum { |s| side_price(s) }
return price
end
class SideNotFoundError < KeyError
def message
#message ||= "Side not found: #{key}"
end
end
class KebabNotFoundError < KeyError
def message
#message ||= "Kebab not found: #{key}"
end
end
end
def total_cost(h, id:, options:, kebab:)
g = h[:Food].find { |g| g[:id] == id }
g[:Kebab].find { |f| f[:name] == kebab }[:value][1..-1].to_f +
g[:sides].sum do |f|
f[:options].sum { |f| options.include?(f[:id]) ? f[:price][1..-1].to_f : 0 }
end
end
total_cost(h, id: "01", options: [1, 3], kebab: 'chicken')
#=> 16.82
total_cost(h, id: "01", options: [1, 3, 4], kebab: 'chicken')
#=> 20.82
The first step results in
g #=> {:id=>"01", :name=>"ABC", :branch=>"London", :platter_cost=>"£40.00",
# :Kebab=>[{:name=>"chicken", :value=>"£8.12"},
# {:terms=>"lamb", :value=>"£9.67"}],
# :sides=>[{:type=>"drinks",
# :options=>[
# {:id=>1, :name=>"Coke", :price=>"£4.70"},
# {:id=>2, :name=>"Pepsi", :price=>"£2.90"},
# {:id=>3, :name=>"Tango", :price=>"£4.00"}
# ]
# },
# {:type=>"chips",
# :options=>[
# {:id=>4, :name=>"Peri-Peri", :price=>"£4.00"}
# ]
# }
# ]
# }
Note: [].sum #=> 0.
Related
Apparently, my ability to think functional withered over time. I have problems to select a sub-dataset from a dataset. I can solve the problem the hacky imperative style, but I believe, there is a sweet functional solution, which I am unfortunately not able to find.
Consider this data structure (tried to not simplify it beyond usability):
class C
attr_reader :attrC
def initialize(base)
#attrC = { "c1" => base+10 , "c2" => base+20, "c3" => base+30}
end
end
class B
attr_reader :attrB
##counter = 0
def initialize
#attrB = Hash.new
#attrB["b#{##counter}"] = C.new(##counter)
##counter += 1
end
end
class A
attr_reader :attrA
def initialize
#attrA = { "a1" => B.new, "a2" => B.new, "a3" => B.new}
end
end
which is created as a = A.new. The complete data set then would be
#<A: #attrA={"a1"=>#<B: #attrB={"b0"=>#<C: #attrC={"c1"=>10, "c2"=>20, "c3"=>30}>}>,
"a2"=>#<B: #attrB={"b1"=>#<C: #attrC={"c1"=>11, "c2"=>21, "c3"=>31}>}>,
"a3"=>#<B: #attrB={"b2"=>#<C: #attrC={"c1"=>12, "c2"=>22, "c3"=>32}>}>}>
which is subject to a selection. I want to retrieve only those instances of B where attrB's key is "b2".
My hacky way would is:
result = Array.new
A.new.attrA.each do |_,va|
result << va.attrB.select { |kb,_| kb == "b2" }
end
p result.reject { |a| a.empty?} [0]
which results in exactly what I intended:
{"b2"=>#<C: #attrC={"c1"=>12, "c2"=>22, "c3"=>32}>}
but I believe there would be a one-liner using map, fold, zip and reduce.
If you want a one-liner:
a.attrA.values.select { |b| b.attrB.keys == %w(b2) }
This returns instances of B. In your question, you're getting attrB values rather than instances of B. If that's what you want, there's this ugly reduce:
a.attrA.values.reduce([]) { |memo, b| memo << b.attrB if b.attrB.keys == %w(b2) ; memo }
I'm not sure what you're trying to do here, though?
I'm learning coding, and one of the assignments is to return keys is return the names of people who like the same TV show.
I have managed to get it working and to pass TDD, but I'm wondering if I've taken the 'long way around' and that maybe there is a simpler solution?
Here is the setup and test:
class TestFriends < MiniTest::Test
def setup
#person1 = {
name: "Rick",
age: 12,
monies: 1,
friends: ["Jay","Keith","Dave", "Val"],
favourites: {
tv_show: "Friends",
things_to_eat: ["charcuterie"]
}
}
#person2 = {
name: "Jay",
age: 15,
monies: 2,
friends: ["Keith"],
favourites: {
tv_show: "Friends",
things_to_eat: ["soup","bread"]
}
}
#person3 = {
name: "Val",
age: 18,
monies: 20,
friends: ["Rick", "Jay"],
favourites: {
tv_show: "Pokemon",
things_to_eat: ["ratatouille", "stew"]
}
}
#people = [#person1, #person2, #person3]
end
def test_shared_tv_shows
expected = ["Rick", "Jay"]
actual = tv_show(#people)
assert_equal(expected, actual)
end
end
And here is the solution that I found:
def tv_show(people_list)
tv_friends = {}
for person in people_list
if tv_friends.key?(person[:favourites][:tv_show]) == false
tv_friends[person[:favourites][:tv_show]] = [person[:name]]
else
tv_friends[person[:favourites][:tv_show]] << person[:name]
end
end
for array in tv_friends.values()
if array.length() > 1
return array
end
end
end
It passes, but is there a better way of doing this?
I think you could replace those for loops with the Array#each. But in your case, as you're creating a hash with the values in people_list, then you could use the Enumerable#each_with_object assigning a new Hash as its object argument, this way you have your own person hash from the people_list and also a new "empty" hash to start filling as you need.
To check if your inner hash has a key with the value person[:favourites][:tv_show] you can check for its value just as a boolean one, the comparison with false can be skipped, the value will be evaluated as false or true by your if statement.
You can create the variables tv_show and name to reduce a little bit the code, and then over your tv_friends hash to select among its values the one that has a length greater than 1. As this will give you an array inside an array you can get from this the first element with first (or [0]).
def tv_show(people_list)
tv_friends = people_list.each_with_object(Hash.new({})) do |person, hash|
tv_show = person[:favourites][:tv_show]
name = person[:name]
hash.key?(tv_show) ? hash[tv_show] << name : hash[tv_show] = [name]
end
tv_friends.values.select { |value| value.length > 1 }.first
end
Also you can omit parentheses when the method call doesn't have arguments.
I have a list of immutable value objects. The lookup class provides ways to iterate and query that data:
class Banker
Bank = Struct.new(:name, :bic, :codes)
attr_reader :banks
def initialize
#banks = [
Bank.new('Citibank', '1234567', ['1', '2']),
Bank.new('Wells Fargo', '7654321', ['4']), # etc.
]
end
def find_by_bic(bic)
banks.each do |bank|
return bank if bank.bic == bic
end
end
end
#banks is initialized every time Banker is used. What options are there to cache #banks so that it's reused across different instances of the Banker?
I don't think Struct buys you anything here. How about doing it like this?
Code
class Banker
#all_banks = {}
class << self
attr_reader :all_banks
end
attr_reader :banks
def initialize(banks)
#banks = banks.keys
banks.each { |k,v| self.class.all_banks[k] = v }
end
def find_by_bic(bic)
return nil unless #banks.include?(bic)
self.class.all_banks[bic]
end
end
Note self in self.class is needed to distinguish the class of self from the keyword class.
Example
b1 = Banker.new({ '1234567' => { name: 'Citibank', codes: ["1", "2"] },
'7654321' => { name: 'Wells Fargo', codes: ['4'] } })
b1.banks
#=> ["1234567", "7654321"]
Banker.all_banks
#=> {"1234567"=>{:name=>"Citibank", :codes=>["1", "2"]},
# "7654321"=>{:name=>"Wells Fargo", :codes=>["4"]}}
b1.find_by_bic '7654321'
#=> {:name=>"Wells Fargo", :codes=>["4"]}
b1.find_by_bic '1234567'
#=> {:name=>"Citibank", :codes=>["1", "2"]}
b1.find_by_bic '0000000'
#=> nil
b2 = Banker.new({ '6523155' => { name: 'Bank of America', codes: ["3"] },
'1234567' => { name: 'Citibank', codes: ["1", "2"] } })
b2.banks
#=> ["6523155", "1234567"]
Banker.all_banks
#=> {"1234567"=>{:name=>"Citibank", :codes=>["1", "2"]},
# "7654321"=>{:name=>"Wells Fargo", :codes=>["4"]},
# "6523155"=>{:name=>"Bank of America", :codes=>["3"]}}
b2.find_by_bic '6523155'
#=> {:name=>"Bank of America", :codes=>["3"]}
b2.find_by_bic '1234567'
#=> {:name=>"Citibank", :codes=>["1", "2"]}
b2.find_by_bic '7654321'
#=> nil
Alternatives
If you prefer you could instead add the class method:
def self.new(banks)
banks.each { |k,v| all_banks[k] = v }
super
end
and remove the first line in initialize.
Or, if you have a complete list of all banks, you could instead just make all_banks a constant:
ALL_BANKS = {"1234567"=>{:name=>"Citibank", :codes=>["1", "2"]},
"7654321"=>{:name=>"Wells Fargo", :codes=>["4"]},
"6523155"=>{:name=>"Bank of America", :codes=>["3"]}}
def find_by_bic(bic)
return nil unless #banks.include?(bic)
ALL_BANKS[bic]
end
and change initialize to:
def initialize(bics)
#banks = bics
end
where bics is an array of bic values.
To share immutable data between instances you can use frozen class variables: ##banks ||= [...].freeze
I have the following class called Tree that builds a simple tree
class Tree
attr_accessor :children, :node_name
def initialize(name, children=[])
#children = children
#node_name = name
end
def visit_all(&block)
visit &block
children.each {|c| c.visit_all &block}
end
def visit(&block)
block.call self
end
end
ruby_tree = Tree.new("grandpa",
[Tree.new("dad", [Tree.new("child1"), Tree.new("child2")]),
Tree.new("uncle", [Tree.new("child3"), Tree.new("child4")])])
puts "Visiting a node"
ruby_tree.visit {|node| puts node.node_name}
puts
puts "visiting entire tree"
ruby_tree.visit_all {|node| puts node.node_name}
Now what I am trying to do is to be able to create a tree as nested hashes instead. For example, for this one this would be:
{'grandpa'=>{'dad'=>{'child 1'=>{},'child 2'=>{}}, 'uncle'=>{'child 3'=>{}, 'child 4'=>{}}}}
Any ideas that could help?
It was melting my brain so I wrote a spec for it:
# encoding: UTF-8
require 'rspec' # testing/behaviour description framework
require_relative "../tree.rb" # pull in the actual code
# Everything in the `describe` block is rspec "tests"
describe :to_h do
# contexts are useful for describing what happens under certain conditions, in the first case, when there is only the top of the tree passed to to_h
context "One level deep" do
# a let is a way of declaring a variable in rspec (that keeps it useful)
let(:ruby_tree) { Tree.new "grandpa" }
let(:expected) { {"grandpa" => {} } }
subject { ruby_tree.to_h } # this the behaviour you're testing
it { should == expected } # it should equal what's in expected above
end
# The next two contexts are just testing deeper trees. I thought that each depth up to 3 should be tested, as past 3 levels it would be the same as 3.
context "Two levels deep" do
let(:ruby_tree) {
Tree.new( "grandpa",
[Tree.new("dad"), Tree.new("uncle") ]
)
}
let(:expected) do
{"grandpa" => {
"dad" => {}, "uncle" => {}
}
}
end
subject { ruby_tree.to_h }
it { should == expected }
end
context "grandchildren" do
let(:ruby_tree){
ruby_tree = Tree.new("grandpa",
[Tree.new("dad", [Tree.new("child1"), Tree.new("child2")]),
Tree.new("uncle", [Tree.new("child3"), Tree.new("child4")])])
}
let(:expected) {
{'grandpa'=>{'dad'=>{'child1'=>{},'child2'=>{}}, 'uncle'=>{'child3'=>{}, 'child4'=>{}}}}
}
subject { ruby_tree.to_h }
it { should == expected }
end
end
class Tree
def to_h
hash ={} # create a hash
# `reduce` is a synonym for `inject`, see the other answer for a link to the docs,
# but it's a type of fold
# http://en.wikipedia.org/wiki/Fold_(higher-order_function),
# which will take a list of several objects and
# fold them into one (or fewer, but generally one) through application of a function.
# It reduces the list through injecting a function, hence the synonyms.
# Here, the current node's list of children is folded into one hash by
# applying Hash#merge to each child (once the child has been been made
# into a one key hash, possibly with children too), and then assigned as
# the current node's hash value, with the node_name as the key.
hash[#node_name] = children.reduce({}){|mem,c| mem.merge c.to_h}
hash # return the hash
end
end
I'm certain this could be done better, but it works at least.
Btw, the hash you provided has some extra spaces in it that I don't think should be there? e.g. "child 1" when it should be "child1", unless you really want that added in?
class Tree
def to_hash
{ #node_name => #children.inject({}) { |acum, child| acum.merge(child.to_hash) } }
end
end
p ruby_tree.to_hash
See documentation for inject here
Break it into simpler subproblems and use recursion:
def make_node(name,subhash)
Tree.new(name,subhash.keys.collect{|k|make_node(k,subhash[k])})
end
def make_root(hash)
make_node(hash.keys[0],hash[hash.keys[0]])
end
Then to prove it works:
tree_like_this = make_root({'grandpa' => { 'dad' => {'child 1' => {}, 'child 2' => {} },
'uncle' => {'child 3' => {}, 'child 4' => {} } } })
puts 'tree like this'
tree_like_this.visit_all{|n|puts n.node_name}
This was an exercise from Seven Languages In Seven Weeks. The original exercise said to put it all in initialize.
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