iterate through nested hashes using conditionals in Ruby - ruby

I am trying to build a new_hash from this hash:
languages = {
:oo => {
:ruby => {
:type => "interpreted"
},
:javascript => {
:type => "interpreted"
},
:python => {
:type => "interpreted"
}
},
:functional => {
:clojure => {
:type => "compiled"
},
:erlang => {
:type => "compiled"
},
:javascript => {
:type => "interpreted"
}
}
}
and the desired result is:
{
:ruby => {
:type => "interpreted",
:style => [:oo]
},
:javascript => {
:type => "interpreted",
:style => [:oo, :functional]
},
:python => {
:type => "interpreted",
:style => [:oo]
},
:clojure => {
:type => "compiled",
:style => [:functional]
},
:erlang => {
:type => "compiled",
:style => [:functional]
}
}
Here is what I've done so far:
def reformat_languages(languages)
new_hash = {}
languages.each do |k, v|
v.each do |k1, v1|
new_hash[k1] = v1
new_hash[k1][:style] = []
new_hash[k1][:style] << k
end
end
new_hash
end
unfortunately, I cannot get the desired result. I understand that when the iteration arrives at the second javascript key, it re-writes over the first iteration giving me:
:javascript => {
:type => "interpreted",
:style => [:functional]
}
instead of:
:javascript => {
:type => "interpreted",
:style => [:oo, :functional]
}
Here is a link of a repl.it where I you can see the code in action: https://repl.it/BebC
I know I need to use a conditional, but I am not really sure where and on what to use it. If somebody could help me getting the desired result and explain a little bit why it works the way it works.

You can use something like
h = {}
languages.each do |k, v| # oo or func
v.each do |k1, v1| # ruby/python
if h[k1]
h[k1][:style] << k
else
h[k1] = {type: v1[:type], style: [k]}
end
end
end
It checks to see that h is defined, and if so, appends to its array. Otherwise it defines the entire hash with your type and a style array of size 1.

There is too much unconditional overwriting going on in your code. Should be something like this instead:
new_hash[k1] ||= {} # init to empty hash
new_hash[k1][:type] = v1[:type]
new_hash[k1][:style] ||= [] # make sure array exists
new_hash[k1][:style] << k
Instead of replacing entire new_hash[k1], you should change individual parts of it.

This is not an answer (so please no upvotes). Rather, it is an extended comment to help you understand the code #Martin suggested. (I see you are new to SO and quite possibly to Ruby as well.) Salting code with puts statements, as I have done, is often quite helpful, even after you become experienced with the language.
languages = {
:oo => {
:ruby => {
:type => "interpreted"
},
:javascript => {
:type => "interpreted"
}
},
:functional => {
:clojure => {
:type => "compiled"
},
:javascript => {
:type => "interpreted"
}
}
}
h = {}
languages.each do |k, v| # oo or func
puts "k=#{k}, v=#{v}"
v.each do |k1, v1| # ruby/python
puts " k1=#{k1}, v1=#{v1}"
if h[k1]
puts " h[#{k1}]=#{h[k1]} (truthy)"
h[k1][:style] << k
puts " h after h[#{k1}][:style] << #{k}: #{h}"
else
puts " h[#{k1}].nil?=true (falsy)"
h[k1] = {type: v1[:type], style: [k]}
puts " h after h[#{k1}] = {type: v1[:type], style: #{k}}: #{h}"
end
end
end
prints:
k=oo, v={:ruby=>{:type=>"interpreted"}, :javascript=>{:type=>"interpreted"}}
k1=ruby, v1={:type=>"interpreted"}
h[ruby].nil?=true (falsy)
h after h[ruby] = {type: v1[:type], :style: oo}:
{:ruby=>{:type=>"interpreted", :style=>[:oo]}}
k1=javascript, v1={:type=>"interpreted"}
h[javascript].nil?=true (falsy)
h after h[javascript] = {type: v1[:type], :style: oo}:
{:ruby=>{:type=>"interpreted", :style=>[:oo]},
:javascript=>{:type=>"interpreted", :style=>[:oo]}}
k=functional, v={:clojure=>{:type=>"compiled"}, :javascript=>{:type=>"interpreted"}}
k1=clojure, v1={:type=>"compiled"}
h[clojure].nil?=true (falsy)
h after h[clojure] = {type: v1[:type], :style: functional}:
{:ruby=>{:type=>"interpreted", :style=>[:oo]},
:javascript=>{:type=>"interpreted", :style=>[:oo]},
:clojure=>{:type=>"compiled", :style=>[:functional]}}
k1=javascript, v1={:type=>"interpreted"}
h[javascript]={:type=>"interpreted", :style=>[:oo]} (truthy)
h after h[javascript][:style] << functional:
{:ruby=>{:type=>"interpreted", :style=>[:oo]},
:javascript=>{:type=>"interpreted", :style=>[:oo, :functional]},
:clojure=>{:type=>"compiled", :style=>[:functional]}}
and returns:
#=> {:oo =>{:ruby=>{:type=>"interpreted"},
# :javascript=>{:type=>"interpreted"}},
# :functional=>{:clojure=>{:type=>"compiled"},
# :javascript=>{:type=>"interpreted"}}}

You are overwriting the hashes generated which is leading to the unexpected behavior you mentioned. The following piece of code does what you need. Its just a slightly modified version of your code.
def reformat_languages(languages)
new_hash = {}
languages.each do |k, v|
v.each do |k1, v1|
new_hash[k1] ||= v1 #ensures we do not overwrite the already generated language hash
new_hash[k1][:style] ||= [] #protecting against re-initialization of the style array
new_hash[k1][:style] << k
end
end
new_hash
end

Related

Group array of hashes by value and retain structure (hash) in Ruby

I have a hash like this:
hash = {
'en-us': {
where: 'USA'
},
'en-us-zone2': {
where: 'USA'
},
'en-nl': {
where: 'Netherlands'
},
'en-pt': {
where: 'Portugal'
},
}
And I tried to group them using group_by:
result = hash.group_by { |k,v| v[:where] }
However, It returns full of array not by array of hashes. So here is expected and actual results:
Actual Result:
{ "USA"=>
[[:"en-us", {:where=>"USA"}], [:"en-us-zone2", {:where=>"USA"}]],
"Netherlands"=>
[[:"en-nl", {:where=>"Netherlands"}]],
"Portugal"=>
[[:"en-pt", {:where=>"Portugal"}]]
}
Expected Result:
{ "USA"=>
[{:"en-us" => {:where=>"USA"}}, {:"en-us-zone2" => {:where=>"USA"}}]
"Netherlands"=>
[{:"en-nl" => {:where=>"Netherlands"}}]
"Portugal"=>
[{:"en-pt" => {:where=>"Portugal"}}]
}
See, Actual is Array of arrays, instead of array of hashes. Hash keys becomes first array element.
How can I group my hash based on :where ?
This should work:
hash.group_by { |k,v| v[:where] }.each { |_, v| v.map! { |array| { array[0] => array[1] } } }
Or with transform_values
hash.group_by { |k,v| v[:where] }.transform_values { |v| v.map { |array| {array[0] => array[1] } } }
https://ruby-doc.org/core-2.4.0/Hash.html#method-i-transform_values
It's ugly but works:
hash = {
'en-us': {
where: 'USA'
},
'en-us-zone2': {
where: 'USA'
},
'en-nl': {
where: 'Netherlands'
},
'en-pt': {
where: 'Portugal'
},
}
hash.
group_by { |_, v| v[:where] }.
map do |k, v|
[
k,
v.map { |a, b| {a => b} }
]
end.to_h
# => {"USA"=>[{:"en-us"=>{:where=>"USA"}}, {:"en-us-zone2"=>{:where=>"USA"}}], "Netherlands"=>[{:"en-nl"=>{:where=>"Netherlands"}}], "Portugal"=>[{:"en-pt"=>{:where=>"Portugal"}}]}

escaping ruby values in hash

I'm trying to use the values of other variable when declaring a ruby hash. Those values are now being escaped as I expected. How can I fix this?
variables
ipa_url, name, version and bundle-identifier
code
data = {
plist: {
dict: {
key: 'items',
array: {
dict: {
key: %w('assets','metadata'),
array: {
dict: [{ key: %w('kind','url'),
string: %w('software-package',
"#{ipa_url") },
{ key: %w('kind','url'),
string: %w('display-image',"#{icon_url.to_s}") },
{ key: %w('kind','url'),
string: %w('full-size-image',
"#{icon_url}") }],
dict: { key: %w('bundle-identifier','bundle-version',
'kind','title'),
string: %w("#{bundle-identifier}","#{version}",
'software',"#{name}")
}
}
}
}
}
}
}
The %w identifier is used to created an array out of a space delimited text:
%w(this is a test)
# => ["this", "is", "a", "test"]
If you want to use string interpolation there, you should use %W instead:
variable = 'test'
%W(this is a #{variable})
# => ["this", "is", "a", "test"]
zenspider goes over this in detail but for SO purposes, here's the breakdown:
%q(no interpolation)
[6] pry(main)> hey
=> "hello"
[7] pry(main)> hash = { 'hi' => %q("#{hey}", 'how are you') }
=> {"hi"=>"\"\#{hey}\", 'how are you'"}
%Q(interpolation and backslashes)
[8] pry(main)> hash = { 'hi' => %Q("#{hey}", 'how are you') }
=> {"hi"=>"\"hello\", 'how are you'"}
%(interpolation and backslashes)
[9] pry(main)> hash = { 'hi' => %("#{hey}", 'how are you') }
=> {"hi"=>"\"hello\", 'how are you'"}
%W(interpolation) as Uri showed:
[7] pry(main)> hash = { 'hi' => %W(#{hey} how are you) }
=> {"hi"=>["hello", "how", "are", "you"]}

Populating a hash from an array

I have this array:
params[:types] = [type1, type2, type3...]
I would like to populate my hash the following way using the above array:
params[:hash] = {
"type1" => {
something: something
},
"type2" => {
something: something
},
}
Using a for loop like for index in i ...params[:types] just populates the hash with the last value in the array.
You can use the each_with_object method to do this:
params = {}
params[:types] = ["type1", "type2", "type3"]
params[:types].each_with_object({}) { |k, h| h[k] = { "something" => "something" } }
That last line will return:
=> {"type1"=>{"something"=>"something"}, "type2"=>{"something"=>"something"}, "type3"=>{"something"=>"something"}}
Here is a code snippet example that does what you need.
hash = {}
array.each do |a|
hash[a.to_s] = { "something" => "something" }
end
output:
hash
=> {
"type1" => {
"something" => "something"
},
"type2" => {
"something" => "something"
},
"type3" => {
"something" => "something"
}
}
You could do this:
params = { types: ["type1", "type2", "type3"] }
Hash[params[:types].product([{"something" => "something"}])]
#=> {"type1"=>{"something"=>"something"},
# "type2"=>{"something"=>"something"},
# "type3"=>{"something"=>"something"}}
or with Ruby 2.1,
params[:types].product([{"something" => "something"}]).to_h
If you want a different hash for each element of params[:types]:
hashes = [{ "something1"=>"something1" }, { "something2"=>"something2" },
{ "something3"=>"something3" }]
then
Hash[params[:types].zip(hashes)]
#=> {"type1"=>{"something1"=>"something1"},
# "type2"=>{"something2"=>"something2"},
# "type3"=>{"something3"=>"something3"}}

evading the use of if statements: shortening the code

I watched the video at https://gorails.com/blog/refactoring-if-statements but was looking for a more concise way of evading the use of multiple if or case statements.
The following works
def process(input)
commands = {
:q => Proc.new { puts "Goodbye" },
:tweet => Proc.new { puts "tweeting" },
:dm => Proc.new { puts "direct messaging"},
:help => Proc.new { puts "helping"}
}
commands[input.to_sym].call
end
process "tweet"
But how could i further shorten this ? I tried the following
def process(input)
commands = {
:q => { puts "Goodbye" },
:tweet => { puts "tweeting" },
:dm => { puts "direct messaging"},
:help => { puts "helping"}
}
commands[input.to_sym].to_proc.call
end
process "tweet"
but then i get the error
# syntax error, unexpected tSTRING_BEG, expecting keyword_do or '{' or '('
# :q => { puts "Goodbye" },
# ^
Any suggestions please ?
Use the lambda syntax
def process(input)
commands = {
:q => ->{ puts "Goodbye" },
:tweet => ->{ puts "tweeting" },
:dm => ->{ puts "direct messaging"},
:help => ->{ puts "helping"}
}
commands[input.to_sym].to_proc.call
end
process "tweet"
Using the new Hash syntax can shorten this further:
def process(input)
{
q: ->{ puts "Goodbye" },
tweet: ->{ puts "tweeting" },
dm: ->{ puts "direct messaging"},
help: ->{ puts "helping"}
}[input.to_sym].call
end
process "tweet"
Use Kernel#proc:
Equivalent to Proc.new
def process(input)
commands = {
:q => proc { puts "Goodbye" },
:tweet => proc { puts "tweeting" },
:dm => proc { puts "direct messaging"},
:help => proc { puts "helping"}
}[input.to_sym].call
end
I am not sure whether what you suggest or I will suggest here improves the elegance or the readability of the code under question but you could shorten it even further by using the hash accessor pattern as in:
def process(input)
commands = {
:q => Proc.new { puts "Goodbye" },
:tweet => Proc.new { puts "tweeting" },
:dm => Proc.new { puts "direct messaging"},
:help => Proc.new { puts "helping"}
}[input.to_sym].call
end

hash deep_merge with arrays

I've searched through all the answers regarding the use of deep_merge; however, I am still having trouble with my particular issue. I'm trying to merge 2 hashes and add a specific key for each match. For example:
UPDATED THE FORMAT
Hash 1:
{
"actions"=> [
{
"causes"=> [
{
"shortDescription"=>"short description for run 1",
"userId"=>"user.a"
}
]
}
],
"artifacts"=> [],
"fullDisplayName"=>"Run #1",
"result"=>"FAILURE",
"changeSet"=> {
"items"=>[],
"kind"=>nil
},
"culprits"=> []
}
Hash 2:
{
"actions"=> [
{
"causes"=> [
{
"shortDescription"=>"short description for run 2",
"userId"=>"user.b"
}
]
}
],
"artifacts"=> [],
"fullDisplayName"=>"Run #2",
"result"=>"FAILURE",
"changeSet"=> {
"items"=>[],
"kind"=>nil
},
"culprits"=> []
}
Key list:
["key-one","key-two"]
I would like the resulting hash to be:
{
"actions"=> [
{
"causes"=> [
{
"shortDescription"=> {
"key-one" => "short description for run 1",
"key-two" => "short description for run 2"
},
"userId"=> {
"key-one" => "user.a",
"key-two" => "user.b"
}
}
]
}
],
"artifacts"=> {
"key-one" => [],
"key-two" => []
},
"fullDisplayName"=> {
"key-one" => "Run #1",
"key-two" => "Run #2"
},
"result"=> {
"key-one" => "FAILURE",
"key-two" => "FAILURE"
},
"changeSet"=> {
"items"=> {
"key-one" => [], "key-two" => []
},
"kind"=> {
"key-one" => nil,
"key-two" => nil
}
},
"culprits"=> {
"key-one" => [],
"key-two" => []
}
}
If h1 and h2 are the two hashes you wish to merge, and h3 is the desired result, I think the following should work:
#merge_key1, #merge_key2 = "key-one", "key-two"
def merge_em(g1, g2)
case g1
when Array
g1.size.times {|i| merge_em(g1[i], g2[i])}
when Hash
g1.keys.each do |k|
v = g1[k]
if (Hash === v || (Array === v && !v.empty?))
merge_em(v, g2[k])
else
g1[k] = {#merge_key1 => v, #merge_key2 => g2[k]}
end
end
end
end
h = Marshal.load(Marshal.dump(h1))
merge_em(h, h2)
p (h == h3) # => true
A few notes:
This solution depends on the hash structures; it may not work if the structures are changed.
Marshal.load(Marshal.dump(h1)) is used to make a "deep copy" of h1, so that h1 is not modified. If h1 can be modified, merge_em(h1, h2) is sufficient, with h1 being the merged hash.
The gem awesome_print gives you a nicely-formatted display of complex structures like these. All you'd need to do here is require 'ap' followed by ap h. Try it!

Resources