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?
Related
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.
I have this array [{:foo=>[{:bar=>[:baz]}]}, :foo, {:foo=>[{:bar=>[:bat]}]}, :bar]
As you can see there are symbols and hashes. What I am trying to do is avoid repetition for each key or keys inside values.
My desired output is :
[{:foo=>[{:bar=>[:baz, :bat]}]}, :bar]
As you can see, there's no repetition of key foo or repetition of foo=>bar.
I've been stuck for hours on this one and I cannot achieve it. Any idea?
The below would somehow work:
input = [{:foo=>[{:bar=>[:baz]}]},
:foo,
{:foo=>[{:bar=>[:bat]}]},
:bar]
builder = ->(value, acc = {}) {
case value
when Hash
value.each_with_object(acc) do |(k, v), acc|
builder.(v, acc[k] ||= {})
end
when Array
value.each_with_object(acc) do |v, acc|
builder.(v, acc)
end
else acc[value] ||= {}
end
}
The above already produces more or less acceptable result:
puts (built = builder.(input)).inspect
#⇒ {:foo=>{:bar=>{:baz=>{}, :bat=>{}}}, :bar=>{}}
To return exactly what you wanted one needs to chain lambdas (there is no way to tell in advance whether the object is a leaf or not):
fixer = ->(acc) {
result = acc.all? { |*v| v.last.empty? } ? acc.keys :
acc.map { |k, v| v.empty? ? k : { k => fixer.(v) } }
result.size == 1 ? result.first : result
}
puts fixer.(built).inspect
#⇒ [{:foo=>{:bar=>[:baz, :bat]}}, :bar]
I believe it’s up to you to play with this code to manage it to suit your needs better.
I have a hash of hashes to display as tree, something like routes. Below, I added an example of an expected result and the result I got.
Example hash:
hash = {
'movies' => {
'action' => {
'2007' => ['video1.avi', 'x.wmv'],
'2008' => ['']
},
'comedy' => {
'2007' => [],
'2008' => ['y.avi']
}
},
'audio' => {
'rock' => {
'2003' => [],
'2004' => ['group', 'group1']
}
}
}
I expected this result:
movies
movies\action
movies\action\2007
movies\action\2007\video1.avi
movies\action\2007\x.wmv
movies\action\2008
movies\comedy\2007
movies\comedy\2008
movies\comedy\2008\y.avi
audio
audio\rock\2003
audio\rock\2004
audio\rock\2004\group
audio\rock\2004\group1
Here are some code I made:
def meth(key, val)
val.each do |key1, val1|
puts "#{key}/#{key1}"
meth(key1, val1) if val1
end
end
hash.each do |key, val|
puts key
meth(key,val)
end
It returns this result:
movies
movies/action
action/2007
2007/video1.avi
2007/x.wmv
action/2008
2008/
movies/comedy
comedy/2007
comedy/2008
2008/y.avi
audio
audio/rock
rock/2003
rock/2004
2004/group
2004/group1
Can anybody explain how to do this?
UPDATE
Thanks for answers. In this case I figured out using this code. The hint was to set key1 to the previous result.
def meth key, val
val.each do |key1, val1|
puts "#{key}/#{key1}"
key1 = "#{key}/#{key1}"
meth(key1, val1) if val1
end
end
you could change the code to:
def meth(key, val)
val.each do |key1, val1|
puts "#{key}/"
if (val1 && val1.is_a?(Hash))
meth(key1, val1)
else
puts "#{val1}"
end
end
end
you are expecting the method to work differently dependant on where it's called but that's not the case. The method does the same regardless of where it's called (e.g. if it's called by it self).
Recursion is the act of deviding one problem into smaller subproblems. There'll always be at least two. In your case the two sub problems is
- print two values
- print the key and iterate a hash
At least one of your subproblems need to end the recursion otherwise it will run forever. In the above case the first subproblem ends the recursion.
You have to keep track of the path as an array:
def meth key, val
val.each do |key1, val1|
puts key.join("/")+"/"+key1
meth(key + [key1], val1) if val1
end
end
meth [], root_of_hash
When I have a nested structure that can contain different types of classes I like to create a case statement so it is easy to define what will happen in different scenarios.
def print_tree(input, path=[])
case input
when Hash then input.flat_map{|x,y| print_tree(y, path+[x])}
when Array then input.empty? ? [path] : input.map{|x| path+[x]}
end
end
puts print_tree(my_hash).map{|z|z.join('/')}
Consider this extension to Enumerable:
module Enumerable
def hash_on
h = {}
each do |e|
h[yield(e)] = e
end
h
end
end
It is used like so:
people = [
{:name=>'fred', :age=>32},
{:name=>'barney', :age=>42},
]
people_hash = people.hash_on { |person| person[:name] }
p people_hash['fred'] # => {:age=>32, :name=>"fred"}
p people_hash['barney'] # => {:age=>42, :name=>"barney"}
Is there a built-in function which already does this, or close enough to it that this extension is not needed?
Enumerable.to_h converts a sequence of [key, value]s into a Hash so you can do:
people.map {|p| [p[:name], p]}.to_h
If you have multiple values mapped to the same key this keeps the last one.
[ {:name=>'fred', :age=>32},
{:name=>'barney', :age=>42},
].group_by { |person| person[:name] }
=> {"fred"=>[{:name=>"fred", :age=>32}],
"barney"=>[{:name=>"barney", :age=>42}]}
Keys are in form of arrays to have a possibility to have a several Freds or Barneys, but you can use .map to reconstruct if you really need.
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