I'm new to Ruby and trying to build a meeting app. I have three arrays containing hashes :
one containing my scheduled meetings with dates and therefore an empty array
of people
one containing the people invited per meeting
and a last one containing the people who refused
This materializes as:
meetings = [
{:id=>"1", :peoples=>[]}
{:id=>"2", :peoples=>[]}
{:id=>"3", :peoples=>[]}
]
invited_peoples = [
{:id=>"1", :peoples=>['Tom', 'Henry', 'Georges', 'Nicolas']}
{:id=>"2", :peoples=>['Arthur', 'Carl']}
]
absent_peoples = [
{:id=>"1", :peoples=>['Henry', 'Georges']}
]
And I would like to have : meetings + invited_peoples - absent_peoples like
meetings_with_participants = [
{:id=>"1", :peoples=>['Tom', 'Nicolas']}
{:id=>"2", :peoples=>['Arthur', 'Carl']}
{:id=>"3", :peoples=>[]}
]
I'm looking for a readable solution but I don't find anyone...
Sorry for my english and thank you in advance,
Nicolas
Define a method to find object by id
def find_by_id array_of_hash, id
array_of_hash.find {|x| x[:id] == id} || {peoples: []}
end
Use map to turn a new array, inside map block just use your logic meetings + invited_peoples - absent_peoples like
result = meetings.map do |item|
id = item[:id]
{id: id, peoples: item[:peoples] + find_by_id(invited_peoples, id)[:peoples] - find_by_id(absent_peoples, id)[:peoples]}
end
Result:
=> [{:id=>"1", :peoples=>["Tom", "Nicolas"]}, {:id=>"2", :peoples=>["Arthur", "Carl"]}, {:id=>"3", :peoples=>[]}]
Create a simple hash
h = meetings.each_with_object({}) { |g,h| h[g[:id]] = g[:peoples] }
#=> {"1"=>[], "2"=>[], "3"=>[]}
Add invitees
invited_peoples.each { |g| h[g[:id]] += g[:peoples] }
Now
h #=> {"1"=>["Tom", "Henry", "Georges", "Nicolas"],
# "2"=>["Arthur", "Carl"], "3"=>[]}
Remove declinees
absent_peoples.each { |g| h[g[:id]] -= g[:peoples] }
Now
h #=> {"1"=>["Tom", "Nicolas"], "2"=>["Arthur", "Carl"],
# "3"=>[]}
Convert the hash to an array of hashes
h.map { |k,v| { :id=> k, :peoples=> v } }
#=> [{:id=>"1", :peoples=>["Tom", "Nicolas"]},
# {:id=>"2", :peoples=>["Arthur", "Carl"]},
# {:id=>"3", :peoples=>[]}]
I initially created a hash and only after processing the invitees and decliners did I convert it to an array of hashes. Doing it that way sped :id lookups for adding and removing people. As a consequence, if n = meetings.size, these calculations have a computational complexity close to O(n), "close to" because hash key lookups have a computational complexity that is nearly O(1) (that is, the time needed to locate a key and its value is nearly constant, regardless of the size of the hash). By contrast, methods that search for a value of :id in invited_peoples and absent_peoples, for each element of meetings, have computational complexity of O(n2).
Related
first of all thank you for helping me with my SQL question, at this point.
Now I'm struggling with another thing, makes me think if I should quit being a programmer to be honest.
Anyway, my problem is : I have an array of hashes( and inside that hash another ) like this:
[
{"A1"=>{:month=>1.0, :balance=>"0.0000", :price=>"9.0000"}},
{"A1"=>{:month=>7.0, :balance=>"34030.0000", :price=>"34030.0000"}},
{"A3"=>{:month=>4.0, :balance=>"34030.0000", :price=>"34030.0000"}},
...
]
What I'm trying to accomplish is that, if there are two values with the same key, ie "A1" add those values into one whole hash, without overwriting the old values and having the month as a key desired output:
[
{"A1"=> { 1 => { :balance=> "0.0000", :price=>"9.0000"} },
{ 7 => { :balance => "34030.0000", :price => "34030.0000" } }},
and so on...
]
Is this posible?
Due the current format of the data you have, you'll need more than a couple of transformations. Most of them based on transforming the values of the resulting hash, after grouping the hashes in the array by their only key:
data
.group_by { |hash| hash.keys.first } # (1)
.transform_values { |value| value.flat_map(&:values) } # (2)
.transform_values { |values| values.index_by { |value| value[:month] } } # (3)
The first transformation is to group the current object, holding an array of hashes, by its only hash key, hence the keys.first, resulting in:
{
"A1"=>[
{"A1"=>{:month=>1.0, :balance=>"0.0000", :price=>"9.0000"}},
{"A1"=>{:month=>7.0, :balance=>"34030.0000", :price=>"34030.0000"}}
],
"A3"=>[{"A3"=>{:month=>4.0, :balance=>"34030.0000", :price=>"34030.0000"}}]
}
The second, is to extract only the values from each hash, in the resulting hash, with arrays of hashes:
{
"A1"=>[
{:month=>1.0, :balance=>"0.0000", :price=>"9.0000"},
{:month=>7.0, :balance=>"34030.0000", :price=>"34030.0000"}
],
"A3"=>[{:month=>4.0, :balance=>"34030.0000", :price=>"34030.0000"}]
}
Then, it just lacks to transform the array of hashes, to simply an hash, whose keys are the value of month:
{
"A1"=>{
1.0=>{:month=>1.0, :balance=>"0.0000", :price=>"9.0000"},
7.0=>{:month=>7.0, :balance=>"34030.0000", :price=>"34030.0000"}
},
"A3"=>{4.0=>{:month=>4.0, :balance=>"34030.0000", :price=>"34030.0000"}}
}
#Sebastian's answer is excellent. For variety, let's also consider an iterative approach. Not sure if it's more efficient or easier to understand, but it's always good to understand multiple perspectives.
Setting up the input data you gave us:
arr = [
{"A1"=>{:month=>1.0, :balance=>"0.0000", :price=>"9.0000"}},
{"A1"=>{:month=>7.0, :balance=>"34030.0000", :price=>"34030.0000"}},
{"A3"=>{:month=>4.0, :balance=>"34030.0000", :price=>"34030.0000"}}]
We create a new empty hash for our results.
new_hash = {}
And now iterating over the original data. We're going to make some assumptions about the form of the data.
# We know each thing is going to be a hash.
arr.each do |hsh|
# Set up some convenient variables for keys and
# values we'll need later.
key = hsh.keys.first
value = hsh.values.first
month = value[:month]
# If the output hash doesn't yet have the key,
# give it the key and assign an empty hash to it.
new_hash[key] ||= {}
# Assign the value to the hash, keyed to the current month.
new_hash[key][month] = value
# ... and get rid of the month key that's now redundant.
new_hash[key][month].delete(:month)
end
And the result is:
{"A1"=>{1.0=>{:balance=>"0.0000", :price=>"9.0000"},
7.0=>{:balance=>"34030.0000", :price=>"34030.0000"}},
"A3"=>{4.0=>{:balance=>"34030.0000", :price=>"34030.0000"}}}
Arguably it would be more useful for the desired return value to be a hash:
h = {"A1"=>{1=>{:balance=> "0.0000", :price=> "9.0000"},
7=>{:balance=>"34030.0000", :price=>"34030.0000"}},
"A3"=>{4=>{:balance=>"34030.0000", :price=>"34030.0000"}}}
That way you could write, for example:
require 'bigdecimal'
BigDecimal(h['A1'][7][:price])
#=> 0.3403e5
See BigDecimal. BigDecimal is generally used in financial calculations because it avoids round-off errors.
This result can be obtained by changing the values of :month to integers in arr:
arr = [
{"A1"=>{:month=>1, :balance=> "0.0000", :price=> "9.0000"}},
{"A1"=>{:month=>7, :balance=>"34030.0000", :price=>"34030.0000"}},
{"A3"=>{:month=>4, :balance=>"34030.0000", :price=>"34030.0000"}}
]
and by computing:
h = arr.each_with_object({}) do |g,h|
k,v = g.flatten
(h[k] ||= {}).update(v[:month]=>v.reject { |k,_| k == :month })
end
See Hash#flatten, Hash#update (aka merge!) and Hash#reject.
One could alternatively write:
h = arr.each_with_object(Hash.new { |h,k| h[k] = {} }) do |g,h|
k,v = g.flatten
h[k].update(v[:month]=>v.reject { |k,_| k == :month })
end
See the form of Hash::new that takes a block.
This is my method that is called by the test function:
def movies_with_directors_set(source)
director = []
director1 = []
hash = {}
while outer_index < source.length do
inner_index = 0
while inner_index < source[outer_index][:movies].length do
hash[:title] = []
hash[:title] = source[outer_index][:movies][inner_index][:title]
hash[:director_name] = []
hash[:director_name] = source[outer_index][:name]
director1 << hash.dup
inner_index +=1
end
director << director1.dup
outer_index += 1
end
return director
end
This is the test code:
describe 'movies_with_directors_set' do
describe 'when given a Hash with keys :name and :movies,' do
describe 'returns an Array of Hashes that represent movies' do
describe 'and each Hash has a :director_name key set with the value that was in :name' do
# This lets "sample_data" be used in the two "it" statements below
let (:test_data) {
[
{ :name => "Byron Poodle", :movies => [
{ :title => "At the park" },
{ :title => "On the couch" },
]
},
{ :name => "Nancy Drew", :movies => [
{ :title => "Biting" },
]
}
]
}
it 'correctly "distributes" Byron Poodle as :director_name of the first film' do
# { :name => "A", :movies => [{ :title => "Test" }] }
# becomes... [[{:title => "Test", :director_name => "A"}], ...[], ... []]
results = movies_with_directors_set(test_data)
expect(results.first.first[:director_name]).to eq("Byron Poodle"),
"The first element of the AoA should have 'Byron Poodle' as :director_name"
end
it 'correctly "distributes" Nancy Drew as :director_name of the last film' do
results = movies_with_directors_set(test_data)
expect(results.last.first[:director_name]).to eq("Nancy Drew"),
"The last element of the AoA should have 'Nancy Drew' as :director_name"
end
end
end
end
end
My method returns an array of hashes but for some reason it does not want to pass the test, as it tells me the second part of the test fails.
It could be that it requires arrays within an array due to the way the test is worded.
source is the database that gets passed into the method. This specific database is shown in the test code.
If you want to transform the structure from a nested director-name/titles into director-name/title pairs, there's a much easier way of going about that. Ruby's strength is in the Enumerable library which makes data transformation really fast, efficient, and easy to express. Here's an approach worth using:
def movies_with_directors(source)
# flat_map will join together the inner arrays into a single contiguous array
source.flat_map do |set|
set[:movies].map do |movie|
{
director_name: set[:name],
title: movie[:title]
}
end
end
end
This produces a very flat, easy to navigate structure like this:
# => [{:director_name=>"Byron Poodle", :title=>"At the park"}, {:director_name=>"Byron Poodle", :title=>"On the couch"}, {:director_name=>"Nancy Drew", :title=>"Biting"}]
Where you can iterate over that and assert more easily without having to do .first.first and such.
When using Ruby always try and think in terms of data transformation, not in terms of loops. There's a multitude of tools in the Enumerable library that can perform complicated operations with a single line of code. In your case you needed a combination of flat_map and map, and you're done.
Your test data and test expectations seem to be out of sync.
You have a :name key, and then expect :director_name to be present in your test hash. Change :director_name to be :name and it should pass:
{ :name => "Nancy Drew", :movies => [
and
expect(results.last.first[:director_name]).to eq("Nancy Drew")
So your failing test is probably saying something like expected nil to be "Nancy Drew", right?
I have a JSON data structure like this...
{
"items": [
{
"person": { // person hash }
},
{
"dog": { // dog hash }
},
{
"fruit": { // fruit hash }
},
{
“person”: { // person hash }
}
]
}
}
Each item in the array contains only one key:value pair. The key is the bot that tells me what type of item the value is.
What I'd like to do is iterate the array and run a different function for each type of item.
So I have something like this...
items = data.dig('items')
items.map do |item|
if person = item.dig('person')
transform_person(person)
elsif dog = item.dig('dog')
transform_dog(dog)
elsif fruit = item.dig('fruit')
transform_fruit(fruit)
end
end
But I feel like there should be a more elegant way to do this?
Apologies. I appear to have left some ambiguity in my question.
The initial array may contain multiple items with the same key. What I am trying to do is map to an array of items that are transformed into what is required by the front end. The input contains a strange structure and info that is not needed by the front end.
So the output array order must match the input array order.
Sorry for the confusion.
First you'll want to define the key preference in a constant:
PECKING_ORDER = %w[ person dog fruit ]
Then you can use that to find it:
def detect(item)
PECKING_ORDER.lazy.map do |key|
[ key, item.dig(key) ]
end.find do |key, v|
v
end
end
Where that can dig up the first item that's found. lazy is used here so it doesn't dig them all up needlessly, just does them one at a time until there's a hit.
This gives you a key/value pair which you can use with dynamic dispatch:
items.each do |item|
key, value = detect(item)
if (key)
send(:"transform_#{key}", value)
end
end
if you know the mapping, you could make a pseudo factory hash:
methods_mapped = {
"person" => ->(person) { do_something_with_person(person) },
"dog" => ->(dog) { do_something_with_dog(dog) },
"fruit" => ->(fruit) { do_something_with_fruit(fruit) }
}
items.map do |item|
key = item.keys.first # what if keys.size > 1 ?
method = methods_mapped.fetch(key)
method.call(item[key])
end
or you could it from the opposite direction:
methods_mapped.each do |key, method|
method.call(items.dig(key))
end
Let f be a given method that takes as an argument a hash. Without loss of generality, suppose it is as follows. This corresponds to the OP's transform_person, transform_dog and transform_fruit methods combined.
def f(h)
case h.keys.first
when :person then "somebody"
when :dog then "doggie"
when :fruit then "juicy"
end
end
Suppose we are also given (no need for dig here)
items = data[:items]
#=> [{:person=>{:name=>"Melba"}},
# {:dog=>{:tricks=>[:roll_over, :shake_a_paw]}},
# {:fruit=>{:good=>"raspberries"}}]
and
key_order = [:bird, :marsupial, :dog, :person]
We wish to find the first element k of key_order for which items contains a hash h for which h.key?(k) #=> true. If such a hash h is found we are to then execute f(h).
First compute a hash key_map.
key_map = items.each_with_object({}) { |g,h| h[g.keys.first] = g }
#=> {:person=>{:person=>{:name=>"Melba"}},
# :dog=>{:dog=>{:tricks=>[:roll_over, :shake_a_paw]}},
# :fruit=>{:fruit=>{:good=>"raspberries"}}}
Then we simply execute
k = key_order.find { |k| key_map[k] }
#=> :dog
k ? f(key_map[k]) : nil
#=> "doggie"
I would kept it simple:
items.map do |item|
do_something_with_person(item) if item.dig('person')
do_something_with_dog(item) if item.dig('dog')
do_something_with_fruit(item) if item.dig('fruit')
end
or
items.each do |item|
case item
when item.dig('person') then do_something_with_person(item)
when item.dig('dog') then do_something_with_dog(item)
when item.dig('fruit') then do_something_with_fruit(item)
end
end
or
def do_something(item)
case
when item.dig('person') then do_something_with_person(item)
when item.dig('dog') then do_something_with_dog(item)
when item.dig('fruit') then do_something_with_fruit(item)
end
end
items.map { |item| do_something(item) }
My input is:
{"id"=>34, "fname"=>"XXX", "department"=>"IT", "email"=>"xxx#example.com"}
schema = [
{:type=>"string", :name=>"email", :control_type=>"email", :label=>"EMAIL"},
{:type=>"string", :name=>"fname", :control_type=>"text", :label=>"FNAME"},
{:type=>"string", :name=>"surname", :control_type=>"text", :label=>"LNAME"}
]
How do I pick only:
{"fname"=>"XXX", "email"=>"xxx#example.com"}
matching the :name of schema array?
Rails provides a Hash method that makes filtering quite simple - slice(). In combination with the splat operator * you could write the following code.
fields = schema.map { |e| e[:name] }
input.slice(*fields)
#=> {"fname"=>"XXX", "email"=>"xxx#example.com"}
You can first generate field names from schema
fields = schema.map{|f| f[:name]}
Then filter out your input:
input = {"id"=>34, "fname"=>"XXX", "department"=>"IT", "email"=>"xxx#example.com"}
input.select{|k, v| fields.include?(k)}
#=> {"fname"=>"XXX", "email"=>"xxx#example.com"}
A web service is returning a hash that contains an unknown number of nested hashes, some of which contain an array, which in turn contains an unknown number of nested hashes.
Some of the keys are not unique -- i.e. are present in more than one of the nested hashes.
However, all the keys that I actually care about are all unique.
Is there someway I can give a key to the top-level hash, and get back it's value even if the key-value pair is buried deep in this morass?
(The web service is Amazon Product Advertising API, which slightly varies the structure of the results that it gives depending on the number of results and the search types permitted in each product category.)
Here's a simple recursive solution:
def nested_hash_value(obj,key)
if obj.respond_to?(:key?) && obj.key?(key)
obj[key]
elsif obj.respond_to?(:each)
r = nil
obj.find{ |*a| r=nested_hash_value(a.last,key) }
r
end
end
h = { foo:[1,2,[3,4],{a:{bar:42}}] }
p nested_hash_value(h,:bar)
#=> 42
No need for monkey patching, just use Hashie gem: https://github.com/intridea/hashie#deepfind
user = {
name: { first: 'Bob', last: 'Boberts' },
groups: [
{ name: 'Rubyists' },
{ name: 'Open source enthusiasts' }
]
}
user.extend Hashie::Extensions::DeepFind
user.deep_find(:name) #=> { first: 'Bob', last: 'Boberts' }
For arbitrary Enumerable objects, there is another extension available, DeepLocate: https://github.com/intridea/hashie#deeplocate
Combining a few of the answers and comments above:
class Hash
def deep_find(key, object=self, found=nil)
if object.respond_to?(:key?) && object.key?(key)
return object[key]
elsif object.is_a? Enumerable
object.find { |*a| found = deep_find(key, a.last) }
return found
end
end
end
Ruby 2.3 introduces Hash#dig, which allows you to do:
h = { foo: {bar: {baz: 1}}}
h.dig(:foo, :bar, :baz) #=> 1
h.dig(:foo, :zot) #=> nil
A variation of barelyknown's solution: This will find all the values for a key in a hash rather than the first match.
class Hash
def deep_find(key, object=self, found=[])
if object.respond_to?(:key?) && object.key?(key)
found << object[key]
end
if object.is_a? Enumerable
found << object.collect { |*a| deep_find(key, a.last) }
end
found.flatten.compact
end
end
{a: [{b: 1}, {b: 2}]}.deep_find(:b) will return [1, 2]
Despite this appearing to be a common problem, I've just spent a while trying to find/come up with exactly what I need, which I think is the same as your requirement. Neither of the links in the first response are spot-on.
class Hash
def deep_find(key)
key?(key) ? self[key] : self.values.inject(nil) {|memo, v| memo ||= v.deep_find(key) if v.respond_to?(:deep_find) }
end
end
So given:
hash = {:get_transaction_list_response => { :get_transaction_list_return => { :transaction => [ { ...
The following:
hash.deep_find(:transaction)
will find the array associated with the :transaction key.
This is not optimal as the inject will continue to iterate even if memo is populated.
I use the following code
def search_hash(hash, key)
return hash[key] if hash.assoc(key)
hash.delete_if{|key, value| value.class != Hash}
new_hash = Hash.new
hash.each_value {|values| new_hash.merge!(values)}
unless new_hash.empty?
search_hash(new_hash, key)
end
end
I ended up using this for a small trie search I wrote:
def trie_search(str, obj=self)
if str.length <= 1
obj[str]
else
str_array = str.chars
next_trie = obj[str_array.shift]
next_trie ? trie_search(str_array.join, next_trie) : nil
end
end
Note: this is just for nested hashes at the moment. Currently no array support.
Because Rails 5 ActionController::Parameters no longer inherits from Hash, I've had to modify the method and make it specific to parameters.
module ActionController
class Parameters
def deep_find(key, object=self, found=nil)
if object.respond_to?(:key?) && object.key?(key)
return object[key]
elsif object.respond_to?(:each)
object = object.to_unsafe_h if object.is_a?(ActionController::Parameters)
object.find { |*a| found = deep_find(key, a.last) }
return found
end
end
end
end
If the key is found, it returns the value of that key, but it doesn't return an ActionController::Parameter object so Strong Parameters are not preserved.