Ruby refactoring: converting array to hash - ruby-on-rails

Here's what I get in Rails params:
obj => {
"raw_data" =>
[
{ "id" => "1", "name" => "John Doe" },
{ "id" => "2", "name" => "Jane Doe" }
]
}
I have to transform into a following object:
obj => {
"data" =>
{
"1" => { "name" => "John Doe" },
"2" => { "name" => "Jane Doe" }
}
}
Here's the code I have working so far:
if obj[:raw_data]
obj[:data] = Hash.new
obj[:raw_data].each do |raw|
obj[:data][raw[:id]] = Hash.new
obj[:data][raw[:id]][:name] = raw[:name] if raw[:name].present?
end
end
obj.delete(:raw_data)
Is there a way to refactor it? Maybe using map. Note that data structure has to change from array to hash as well.
Thanks for any tips.

Here's one way:
obj = {
"raw_data" => [
{ "id" => "1", "name" => "John Doe" },
{ "id" => "2", "name" => "Jane Doe" }
]
}
data = obj["raw_data"].map do |item|
item = item.dup
[ item.delete('id'), item ]
end
obj2 = { "data" => data.to_h }
# => { "data" =>
# { "1" => { "name" => "John Doe" },
# "2" => { "name" => "Jane Doe" }
# }
# }
If you're using Rails you can use the Hash#except method from ActiveSupport to make it a little more succinct:
data = obj["raw_data"].map {|item| [ item["id"], item.except("id") ] }
obj2 = { "data" => data.to_h }

d = obj[:raw_data]
keys = d.map { |h| h["id"] }
values = d.map { |h| h.except("id") }
Hash[ keys.zip(values) ]
# or as a oneliner
Hash[ d.map { |h| h["id"] }.zip(d.map { |h| h.except("id")) ]
# => {"1"=>{"name"=>"John Doe"}, "2"=>{"name"=>"Jane Doe"}}
This special Hash[] syntax lets you create a hash from a array of keys and an array of values.
Hash.except(*args) is an ActiveSupport addition to the hash class which returns a new key without the keys in the blacklist.

In rails, you can use index_by method:
obj = {raw_data: [{id: "1", name: "John Doe"}, {id: "2", name: "Jane Doe"}]}
obj2 = {
data: obj[:raw_data].index_by {|h| h[:id]}.each {|_,h| h.delete(:id)}
} #=> {:data=>{"1"=>{:name=>"John Doe"}, "2"=>{:name=>"Jane Doe"}}}
One downfall of this is that it will modify the original data by deleting id property. If this is unacceptable, here is modified, safe version:
obj2 = {
data: obj[:raw_data].map(&:clone).index_by {|h| h[:id]}.each {|_,h| h.delete(:id)}
} #=> {:data=>{"1"=>{:name=>"John Doe"}, "2"=>{:name=>"Jane Doe"}}}

I assume you mean obj = {...} and not obj => {...}, as the latter is not a valid object. If so:
{ "data" => obj["raw_data"].each_with_object({}) { |g,h|
h[g["id"]] = g.reject { |k,_| k == "id" } } }
#=> {"data"=>{"1"=>{"name"=>"John Doe"}, "2"=>{"name"=>"Jane Doe"}}}
If obj can be mutated, you can simplify a bit:
{ "data" => obj["raw_data"].each_with_object({}) { |g,h| h[g.delete("id")]=g } }
As an improved non-mutating solution, #Max suggested a Rails' tweak:
{ "data" => obj["raw_data"].each_with_object({}) { |g,h| h[g["id"]] = g.except("id") } }
That looks good to me, but as I don't know rails, I'm taking that advice at face value.

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

Return an element in array if condition is met

Given this array:
[[{"RepCode"=>"AL20", "ID"=>"eae71dff-3796-4c61-956e-a75a00b01a7b", "Name"=>"Schuh, Eddy", "Folios"=>[]}],
[{"RepCode"=>"ABNX", "ID"=>"637e9117-ed03-45ef-8950-a7220087ee9a", "Name"=>"Eckerson, Kathy", "Folios" => [{"ID"=>"d0cda2be-c142-47d1-9a81-a76c0eea2765"}],
[{"RepCode"=>"ABCD", "ID"=>"637e9117-ed03-45ef-8950-a234902038", "Name"=>"Sarah, Barber", "Folios" => [{"ID"=>"46aafe31-f686-49e2-9d58-c694ea55c14f"}]]
I need to return the ONE array that matches the given id for the Folio key
CODE:
correct_manager = managers.detect do |manager|
manager.first["Folios"].map { |f| f["ID"] == "d0cda2be-c142-47d1-9a81-a76c0eea2765" }
end
That returns:
{"RepCode"=>"AL20", "ID"=>"eae71dff-3796-4c61-956e-a75a00b01a7b", "Name"=>"Schuh, Eddy", "Folios"=>[]}
And I want it to return
{"RepCode"=>"ABNX", "ID"=>"637e9117-ed03-45ef-8950-a7220087ee9a", "Name"=>"Eckerson, Kathy", "Folios" => [{"ID"=>"d0cda2be-c142-47d1-9a81-a76c0eea2765"}
because the ID's match in the detect method.
How can I return the one array that matches the passed in id?
You can use Enumerable #find
correct_manager = managers.find do |manager|
folios = manager.first["Folios"][0] || {}
folios["ID"] == "d0cda2be-c142-47d1-9a81-a76c0eea2765"
end
First let's format your data correctly:
managers = [
[ {"RepCode"=>"AL20", "ID"=>"eae71dff-3796-4c61-956e-a75a00b01a7b", "Name"=>"Schuh, Eddy", "Folios"=>[] } ],
[ {"RepCode"=>"ABNX", "ID"=>"637e9117-ed03-45ef-8950-a7220087ee9a", "Name"=>"Eckerson, Kathy", "Folios" => [{"ID"=>"d0cda2be-c142-47d1-9a81-a76c0eea2765"}] } ],
[ {"RepCode"=>"ABCD", "ID"=>"637e9117-ed03-45ef-8950-a234902038", "Name"=>"Sarah, Barber", "Folios" => [{"ID"=>"46aafe31-f686-49e2-9d58-c694ea55c14f"}] } ]
]
target_id = 'd0cda2be-c142-47d1-9a81-a76c0eea2765'
managers.flatten.find{|k,_v| k['Folios'].any?{|f| f.key?('ID') && f['ID'] == target_id}}
def doit(managers, val)
managers.find { |(h)| h["Folios"] == ["ID"=>val] }
end
managers = [
[{ "RepCode"=>"AL20", "Folios"=>[] }],
[{ "RepCode"=>"ABNX", "Folios"=>[{ "ID"=>"d0cda2be-c142-47d1-9a81" }] }],
[{ "RepCode"=>"ABCD", "Folios"=>[{ "ID"=>"46aafe31-f686-49e2-9d58" }] }]
]
doit(managers, "d0cda2be-c142-47d1-9a81")
# => [{"RepCode"=>"ABNX", "Folios"=>[{"ID"=>"d0cda2be-c142-47d1-9a81"}]}]

How to convert hash with keys representing nesting into a nested hash

I need to convert the following hash:
{
"item[0][size]" => "12",
"item[0][count]" => "1"
}
to this:
{
"item": {
"0": {
"size": "12",
"count": "1"
}
}
}
Could you please advice on how to achieve that most gracefully? Maybe I can reuse some ActionPack's utility method that is used for parsing parameter strings?
You can reuse a rack lib method Rack::Utils.parse_nested_query
require "rack"
def p p
Rack::Utils.parse_nested_query(p)
end
p 'item[0][size]=12' # => {"item"=>{"0"=>{"size"=>"12"}}}
Found here.
After some research I found a way to parse nested query keys using http://apidock.com/rails/Rack/Utils/parse_nested_query:
Rack::Utils.parse_nested_query('item[0][size]')
=> {
"item" => {
"0" => {
"size" => nil
}
}
}
So it's now possible to do:
items_string = item_hash.to_a.map { |row| row.join('=') }.join('&')
result = Rack::Utils.parse_nested_query(items_string)
=> {
"item" => {
"0" => {
"size" => "12",
"count" => "1"
}
}
}

remove the key and value in the params hash with ruby on rails

hello how is to remove empty value or nil in ruby on rails
I try to reject the method but it does not work there are there any other method?
Here is my hash with empty values
{
first_name: {
1: "david",
2: ""
},
last_name: {
1: "david",
2: ""
},
role: {
1: "dev",
2: ""
},
bio: {
1: "commercial",
2: ""
},
thank you
hash.each {|_, v| v.delete_if {|_, v| v == ""}}
#=> {"first_name"=>{1=>"david"}, "last_name"=>{1=>"david"}, "role"=>{1=>"dev"}, "bio"=>{1=>"commercial"}}
class Hash
def compact(opts={})
inject({}) do |new_hash, (k,v)|
if !v.blank?
new_hash[k] = opts[:recursive] && v.class == Hash ? v.compact(opts) : v
end
new_hash
end
end
end
hash = {
:first_name=> {
1=> "david",
2=> ""
},
:last_name=> {
1=> "david",
2=> ""
},
:role=> {
1=> "dev",
2=> ""
},
:bio=> {
1=> "commercial",
2=> ""
}
}
hash.compact(:recursive=>true)
will give
{
:first_name => {
1 => "david"
},
:last_name => {
1 => "david"
},
:role => {
1 => "dev"
},
:bio => {
1 => "commercial"
}
}
source: Removing all empty elements from a hash / YAML?

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