Get specific information from multi-dimensional array - ruby-on-rails

Let say I have a array in this format:
arr = [{
"id":"11",
"children":[
{ "id":"9"},
{ "id":"5", "children":[ {"id":"4"} ] }
]
},
{
"id":"10",
"children":[{ "id":"7"} ]
}
]
And now I would like to get all ID's that are apparent in this array:
11,9,5,4,10,7
For that I would use a recursive code similar to this one:
ids = []
def find_ids arr
arr.each do |entry|
ids << entry["id"] if entry["id"]
find_ids(entry["children"]) if entry["children"]
end
end
What would you do to get the ids?
Do you maybe know a really short version?
Thanks

def grab_ids(arr)
arr.each_with_object([]) do |h,a|
h.each do |k,v|
case v
when Array
a.concat(grab_ids(v))
else
a << v if k == :id
end
end
end
end
grab_ids arr
#=> ["11", "9", "5", "4", "10", "7"]

Other way is to use lambda:
def get_ids(arr)
p = ->(a, exp) do
a.each do |hsh|
exp << hsh["id"]
p.(hsh["children"], exp) if Array === hsh["children"]
end
exp
end
p.(arr, [])
end
get_ids(arr)
# => ["11", "9", "5", "4", "10", "7"]

Or if you wanna to use shorter version:
def get_ids(arr)
p =->(hsh) { Array === hsh["children"] ? ([hsh["id"]] + hsh["children"].map(&p)) : hsh["id"] }
arr.map(&p).flatten
end
get_ids(arr)
# => ["11", "9", "5", "4", "10", "7"]

Related

How to map ruby hashes correctly based on key provided

My data is like:
h = { themes_data: {
Marketing: [
{
id: 68,
projectno: "15",
}
],
Produktentwicklung: [
{
id: 68,
projectno: "15",
},
{
id: 4,
projectno: "3",
}
],
Marketing_summary: [
{
ges: {
result: "47.6"
},
theme: "Marketing"
}
],
Produktentwicklung_summary: [
{
ges: {
result: "87.7"
},
theme: "Produktentwicklung"
}
]
}
}
And my output should be like:
{ "marketing" => [
{
id: 68,
projectno: "15",
},
{
ges: {
result: "47.6"
},
theme: "Marketing"
}
],
"Produktentwicklung" => [
{
id: 68,
projectno: "15"
},
{
id: 4,
projectno: "3",
},
{
ges: {
result: "87.7"
},
theme: "Produktentwicklung"
}
]
}
Code:
def year_overview_theme
branch_hash = {}
#themes_data.each do |td|
arr = []
td[1].map do |dt|
arr << [{content: dt[:projectno], size: 5, align: :right, background_color: 'D8E5FF'}]
end
branch_hash["#{td[0]}"] = arr
end
branch_hash
end
The problem is that it does not iterate for right hash key.
For example, i want like:
marketing + marketing_summary as 1 hash and similarly
Produktentwicklung = Produktentwicklung_summary as one hash but there is some problem in my logic.
Is there a way that I can check like after 2 iteration,
it should do arr << data with branch_hash["#{td[0]}"] = arr ?
The desired hash can be constructed as follows.
h[:themes_data].each_with_object({}) { |(k,v),g|
g.update(k.to_s[/[^_]+/]=>v) { |_,o,n| o+n } }
#=> { "Marketing"=>[
# {:id=>68, :projectno=>"15"},
# {:ges=>{:result=>"47.6"}, :theme=>"Marketing"}
# ],
# "Produktentwicklung"=>[
# {:id=>68, :projectno=>"15"},
# {:id=>4, :projectno=>"3"},
# {:ges=>{:result=>"87.7"}, :theme=>"Produktentwicklung"}
# ]
# }
This uses the form of Hash#update (aka merge) that employs a block to determine the values of keys that are present in both hashes being merged. Here that block is:
{ |_,o,n| o+n }
The first block variable, _, is the common key. I have represented it with an underscore (a valid local variable) to tell the reader that it is not used in the block calculation. That is common practice. The values of the other two block variables, o and n, are explained at the link for the method update.
The regular expression /[^_]+/, matches one or more characters from the start of the string that are not (^) underscores. When used with the method String#[], we obtain:
"Marketing"[/[^_]+/] #=> "Marketing"
"Marketing_summary"[/[^_]+/] #=> "Marketing"
Let me start with a note: This looks to me like something that should rather be solved in SQL (if it's coming from SQL) instead of Ruby.
With that out of the way, here's a solution that should work:
output = {}
themes_data.each do |theme, projects|
projects.each do |project|
key = project[:theme] || theme.to_s
output[key] ||= [] # make sure the target is initialized
output[key] << project
end
end
There would probably be more elegant solutions using reduce or each_with_object but this works and it's simple enough.
keys = themes_data.keys
summary_keys = themes_data.keys.grep(/_summary/)
result = {}.tap do |hash|
(keys - summary_keys).each do |key|
hash[key] = themes_data[key] + themes_data["#{key}_summary".to_sym]
end
end

Ruby sum of elements from similar hashes [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 3 years ago.
Improve this question
Suppose I have these records, where r_years could have any number of year keys:
Item.select('id','name','r_years').where(name:"N1")
...
"id" => 1, "name" => "N1", "r_years" => {"year2020" => "1","year2021" => "2", ...}
"id" => 2, "name" => "N1", "r_years" => {"year2020" => "2","year2021" => "3", ...}
...
How can I get the sum of the r_years values, for every year key, like this:
#r_years_sum = {"year2020" => "3","year2021" => "5", ...}
You can map your result to get only the r_years values, and then use reduce and merge every value for each year and sum their value as an integer:
items.map do |item|
item['r_years']
end.reduce do |acc, item|
item.merge(acc) { |_, oldval, newval| (oldval.to_i + newval.to_i).to_s }
end
# {"year2020"=>"3", "year2021"=>"5"}
arr = [
{ "id"=>"1", "r_years"=>{ "2020"=>"1", "2021"=> "2", "2022"=>"3" } },
{ "id"=>"2", "r_years"=>{ "2020"=>"4", "2021"=> "5", "2022"=>"6" } },
{ "id"=>"3", "r_years"=>{ "2020"=>"7", "2021"=> "8", "2022"=>"9" } }
]
arr.each_with_object(Hash.new(0)) do |g,h|
g["r_years"].each { |k,v| h[k] += v.to_i }
end.transform_values(&:to_s)
#=> {"2020"=>"12", "2021"=>"15", "2022"=>"18"}
The first step is:
arr.each_with_object(Hash.new(0)) do |g,h|
g["r_years"].each { |k,v| h[k] += v.to_i }
end
#=> {"2020"=>12, "2021"=>15, "2022"=>18}
Hash#transform_values is then used to convert the values to strings.
This uses the second form of Hash::new, which takes an argument that is referred to as the default value. h[c] += 1 expands to h[c] = h[c] + 1. If h does not have a key c, h[c] on the right of the equality returns the default value of zero, yielding h[c] = 0 + 1.
You can try this ...
arr = [
{ "id"=>"1", "r_years"=>{ "2020"=>"1", "2021"=> "2", "2022"=>"3" } },
{ "id"=>"2", "r_years"=>{ "2020"=>"4", "2021"=> "5", "2022"=>"6" } },
{ "id"=>"3", "r_years"=>{ "2020"=>"7", "2021"=> "8", "2022"=>"9" } }
]
res_hash=Hash.new(0)
arr.each do |x|
x["r_years"].select{ |key,value| res_hash[key]+=value.to_i }
end

Iterate through a hash. However, my value is changing every time

I'm currently working on a simple hash loop, to manipulate some json data. Here's my Json data:
{
"polls": [
{ "id": 1, "question": "Pensez-vous utiliser le service de cordonnerie/pressing au moins 2 fois par mois ?" },
{ "id": 2, "question": "Avez-vous passé une bonne semaine ?" },
{ "id": 3, "question": "Le saviez-vous ? Il existe une journée d'accompagnement familial." }
],
"answers": [
{ "id": 1, "poll_id": 1, "value": true },
{ "id": 2, "poll_id": 3, "value": false },
{ "id": 3, "poll_id": 2, "value": 3 }
]
}
I want to have the poll_id value and the value from the answers hash. So here's what I code :
require 'json'
file = File.read('data.json')
datas = JSON.parse(file)
result = Hash.new
datas["answers"].each do |answer|
result["polls"] = {"id" => answer["poll_id"], "value" => answer["value"]}
end
polls_json = result.to_json
However, it returns me :
{
"polls": {
"id": 2,
"value": 3
}
}
Here's the output i am looking for :
{
"polls": [
{
"id": 1,
"value": true
},
{
"id": 2,
"value": 3
},
{
"id": 3,
"value": false
}
]
}
It seems that the value is not saved into my loop. I've tried different method but I still cannot find a solution .. Any suggestions?
You should be using reduce here, i.e.
datas["answers"].reduce({ polls: [] }) do |hash, data|
hash[:polls] << { id: data["poll_id"], value: data["value"] }
hash
end
This method iterates through the answers, making available the object supplied to reduce (in this case a hash with a :polls array) to which we pass each data hash.
I'd personally, um, reduce this a little further with the following, although it's at some cost to readability:
datas["answers"].reduce({ polls: [] }) do |hash, data|
hash.tap { |h| h[:polls] << { id: data["poll_id"], value: data["value"] } }
end
It's the cleanest method to achieve what you're looking for, using a built-for-purpose method.
Docs for reduce here: https://ruby-doc.org/core-2.1.0/Enumerable.html#method-i-reduce
(I'd also be inclined to update the variable names - data is already plural, so 'datas' is a little confusing to anyone else coming to your code.)
Edit: #max makes a great point re symbol / string keys from your data - keep that in mind if you attempt to apply this.
try the below:
require 'json'
file = File.read('data.json')
datas = JSON.parse(file)
result = Hash.new
poll_json = []
datas["answers"].each do |answer|
poll_json << {"id" => answer["poll_id"], "value" => answer["value"]}
end
p "json = "#{poll_json}"
{
polls: datas["answers"].map do |a|
{ id: a["poll_id"], value: a["value"] }
end
}
In general use .map to iterate through arrays and hashes and return new objects. .each should only be used when you are only concerned about the side effects (like in a view when you are outputting values).
require 'json'
json = JSON.parse(File.read('data.json'))
result = {
polls: json["answers"].map do |a|
{ id: a["poll_id"], value: a["value"] }
end
}
puts result.to_json
The output is:
{"polls":[{"id":1,"value":true},{"id":3,"value":false},{"id":2,"value":3}]}

array of hashes as hash key and value as array using jbuilder

I'm trying to generate JSON response using Jbuilder
I have an array of hashes like this
words= [
{"term": "abc",
"definition": "123"
} ,
{"term": "abc",
"definition": "345"
} ,
{"term": "xyz",
"definition": "890"
}
]
I would like covert this into JSON.
logic here is take all terms as keys and push it's definitions into arrays
{
"abc": ["123","345"],
“xyz”: ["890"]
}
What I achieved so far is
words.each do |word|
json.set! word['text'] ,word['definition']
end
gives me
{
"abc": "123"
"abc": "345",
"xyz": "890"
}
Could some help me on this.
simplest solution :)
words= [
{"term": "abc",
"definition": "123"
} ,
{"term": "abc",
"definition": "345"
} ,
{"term": "xyz",
"definition": "890"
}
]
result_hash = Hash.new{|hsh, key| hsh[key]=[] }
words.map{|x| result_hash[x[:term]].push(x[:definition])}
your output will be in result_hash
You are looking for something like this,
words = [{:term=>"abc", :definition=>"123"}, {:term=>"abc", :definition=>"345"}, {:term=>"xyz", :definition=>"890"}]
words.inject({}) do |h, w|
h[w[:term]] ||= []
h[w[:term]] << w[:definition]
h
end
#=> {"abc"=>["123", "345"], "xyz"=>["890"]}
words.group_by{|d| d[:term]}.map{|k,v| {k => v.map{|val| val[:definition]}}}.reduce(&:merge)
words.map(&:values).group_by(&:shift).each do |k, values|
json.set! k, values.flatten
end
If the order of :term and :definition is not guaranteed, there is an intermediate call to .map(&:sort) on the original hash required, and :shift should be read as :pop since after sorting :definitions will preceed :terms.

Hash remove all except specific keys

I would like to remove every key from a hash except a given key.
For example:
{
"firstName": "John",
"lastName": "Smith",
"age": 25,
"address":
{
"streetAddress": "21 2nd Street",
"city": "New York",
"state": "NY",
"postalCode": "10021"
},
"phoneNumber":
[
{
"type": "home",
"number": "212 555-1234"
},
{
"type": "fax",
"number": "646 555-4567"
}
]
}
I want to remove everything except "firstName" and/or "address".
What about slice?
hash.slice('firstName', 'lastName')
# => { 'firstName' => 'John', 'lastName' => 'Smith' }
Available in Ruby since 2.5
Some other options:
h.select {|k,v| ["age", "address"].include?(k) }
Or you could do this:
class Hash
def select_keys(*args)
select {|k,v| args.include?(k) }
end
end
So you can now just say:
h.select_keys("age", "address")
If you use Rails, please consider ActiveSupport except() method: http://apidock.com/rails/Hash/except
hash = { a: true, b: false, c: nil}
hash.except!(:c) # => { a: true, b: false}
hash # => { a: true, b: false }
Hash#select does what you want:
h = { "a" => 100, "b" => 200, "c" => 300 }
h.select {|k,v| k > "a"} #=> {"b" => 200, "c" => 300}
h.select {|k,v| v < 200} #=> {"a" => 100}
Edit (for comment):
assuming h is your hash above:
h.select {|k,v| k == "age" || k == "address" }
hash = { a: true, b: false, c: nil }
hash.extract!(:c) # => { c: nil }
hash # => { a: true, b: false }
Inspired by Jake Dempsey's answer, this one should be faster for large hashes, as it only peaks explicit keys rather than iterating through the whole hash:
class Hash
def select_keys(*args)
filtered_hash = {}
args.each do |arg|
filtered_hash[arg] = self[arg] if self.has_key?(arg)
end
return filtered_hash
end
end
No Rails needed to get a very concise code:
keys = [ "firstName" , "address" ]
# keys = hash.keys - (hash.keys - keys) # uncomment if needed to preserve hash order
keys.zip(hash.values_at *keys).to_h

Resources