Nested hash iteration: How to iterate a merge over an ( (array of hashes) within a hash ) - ruby

I'm trying to do as the title says. Here is my code:
school.each { |x| school[:students][x].merge!(semester:"Summer") }
I think I pinpointed the problem to the "[x]" above. If I substitute an array position such as "[2]" it works fine. How can make the iteration work?
If the info above is not enough or you'd like to offer a better solution, please see the details below. Thanks!
The error message I get:
file.rb:31:in []': no implicit conversion of Array into Integer (TypeError)
from file.rb:31:inblock in '
from file.rb:31:in each'
from file.rb:31:in'
The nested hash below before alteration:
school = {
:name => "Happy Funtime School",
:location => "NYC",
:instructors => [
{:name=>"Blake", :subject=>"being awesome" },
{:name=>"Ashley", :subject=>"being better than blake"},
{:name=>"Jeff", :subject=>"karaoke"}
],
:students => [
{:name => "Marissa", :grade => "B"},
{:name=>"Billy", :grade => "F"},
{:name => "Frank", :grade => "A"},
{:name => "Sophie", :grade => "C"}
]
}
I'm trying to append :semester=>"Summer" to each of the last four hashes. Here is what I'm trying to go for:
# ...preceding code is the same. Changed code below...
:students => [
{:name => "Marissa", :grade => "B", :semester => "Summer"},
{:name=>"Billy", :grade => "F", :semester => "Summer"},
{:name => "Frank", :grade => "A", :semester => "Summer"},
{:name => "Sophie", :grade => "C", :semester => "Summer"}
]
}

Just iterate over the students:
school[:students].each { |student| student[:semester] = "Summer" }
Or, using merge:
school[:students].each { |student| student.merge!(semester: "Summer") }

The issue is that when you do array.each {|x| do something}, x actually refers to each element in the array.
For example, in the first iteration of the loop,
x = {:name => "Marissa", :grade => "B"}
So what you are really doing is trying to reference:
school[:student][{:name => "Marissa", :grade => "B"}]
Which will not work
What you could do instead is create a for loop to track the index.
for i in 0 ... school[:student].count
school[:students][i].merge!(semester:"Summer")
end
Edit: Stefan's solution is much better than mine, but I will leave this up to show where you went wrong.

I would do as below using Hash#store :
require 'awesome_print'
school = {
:name => "Happy Funtime School",
:location => "NYC",
:instructors => [
{
:name => "Blake",
:subject => "being awesome"
},
{
:name => "Ashley",
:subject => "being better than blake"
},
{
:name => "Jeff",
:subject => "karaoke"
}
],
:students => [
{
:name => "Marissa",
:grade => "B"
},
{
:name => "Billy",
:grade => "F"
},
{
:name => "Frank",
:grade => "A"
},
{
:name => "Sophie",
:grade => "C"
}
]
}
school[:students].each{|h| h.store(:semester ,"Summer")}
ap school,:index => false,:indent => 10
output
{
:name => "Happy Funtime School",
:location => "NYC",
:instructors => [
{
:name => "Blake",
:subject => "being awesome"
},
{
:name => "Ashley",
:subject => "being better than blake"
},
{
:name => "Jeff",
:subject => "karaoke"
}
],
:students => [
{
:name => "Marissa",
:grade => "B",
:semester => "Summer"
},
{
:name => "Billy",
:grade => "F",
:semester => "Summer"
},
{
:name => "Frank",
:grade => "A",
:semester => "Summer"
},
{
:name => "Sophie",
:grade => "C",
:semester => "Summer"
}
]
}

Related

How to count values in a array of hashes

I have an array of hashes
[ {:name => "bob", :type => "some", :product => "apples"},
{:name => "ted", :type => "other", :product => "apples"},....
{:name => "Will", :type => "none", :product => "oranges"} ]
and was wondering if there is a simple way to count the number of product's and store the count as well as the value in an array or hash.
I want the result to be something like:
#products = [{"apples" => 2, "oranges => 1", ...}]
You can do as
array = [
{:name => "bob", :type => "some", :product => "apples"},
{:name => "ted", :type => "other", :product => "apples"},
{:name => "Will", :type => "none", :product => "oranges"}
]
array.each_with_object(Hash.new(0)) { |h1, h2| h2[h1[:product]] += 1 }
# => {"apples"=>2, "oranges"=>1}
You can use Enumerable#group_by and Enumerable#map
array.group_by{|h| h[:product]}.map{|k,v| [k, v.size]}.to_h
# => {"apples"=>2, "oranges"=>1}
While not exactly what the OP was looking for, this may be helpful to many. If you're just looking for the count of a specific product, you could do this:
array = [
{:name => "bob", :type => "some", :product => "apples"},
{:name => "ted", :type => "other", :product => "apples"},
{:name => "Will", :type => "none", :product => "oranges"}
]
array.count { |h| h[:product] == 'apples' }
# => 2
You could count:
hashes = [
{:name => "bob", :type => "some", :product => "apples"},
{:name => "ted", :type => "other", :product => "apples"},
{:name => "Will", :type => "none", :product => "oranges"}
]
hashes.inject(Hash.new(0)) { |h,o| h[o[:product]] += 1; h }
Or maybe...
hashes.instance_eval { Hash[keys.map { |k| [k,count(k)] }] }
I do not know which is the more performant, the latter seims weird to read though.
I would do:
items =[ {:name => "bob", :type => "some", :product => "apples"},
{:name => "ted", :type => "other", :product => "apples"},
{:name => "Will", :type => "none", :product => "oranges"} ]
counts = items.group_by{|x|x[:product]}.map{|x,y|[x,y.count]}
p counts #=> [["apples", 2], ["oranges", 1]]
Then if you need it as a Hash just do:
Hash[counts]

XML nodes getting deleted when converting nested hash to XML in Ruby

I use the following code to convert hashes into XML:
class Hash
def to_xml
map do |k,v|
text = Hash === v ? v.to_xml : v
"<%s>%s</%s>" % [k,text,k]
end.join
end
def to_xml_with_namespace(ns)
map do |k,v|
text = Hash === v ? v.to_xml_with_namespace(ns) : v
"<#{ns}:%s>%s</#{ns}:%s>" % [k,text,k]
end.join
end
end
Everything works fine until I have multiple nodes with the same values, example:
{:users => {
:name_age_node => {:name => "Bob", :age => 50},
:name_age_node => {:name => "Tom", :age => 45},
:name_age_node => {:name => "Jess", :age => 22}
}
What outputs is simply the last node only.. The other nodes get overwritten for some reason. All nodes where there are not duplicate nodes with the same name are fine, whether they are nested or not.
Any ideas on why this could be happening?
You're dealing with hashes. A hash only supports a single occurrence of a particular key:
foo = {:users => {
:name_age_node => {:name => "Bob", :age => 50},
:name_age_node => {:name => "Tom", :age => 45},
:name_age_node => {:name => "Jess", :age => 22}
}
}
foo
# => {:users=>{:name_age_node=>{:name=>"Jess", :age=>22}}}
With unique keys:
foo = {:users => {
:name_age_node1 => {:name => "Bob", :age => 50},
:name_age_node2 => {:name => "Tom", :age => 45},
:name_age_node3 => {:name => "Jess", :age => 22}
}
}
foo
# => {:users=>
# {:name_age_node1=>{:name=>"Bob", :age=>50},
# :name_age_node2=>{:name=>"Tom", :age=>45},
# :name_age_node3=>{:name=>"Jess", :age=>22}}}
Or you could use an array of hashes to contain the inner data:
foo = {:users => [
{:name => "Bob", :age => 50},
{:name => "Tom", :age => 45},
{:name => "Jess", :age => 22}
]
}
foo
# => {:users=>
# [{:name=>"Bob", :age=>50},
# {:name=>"Tom", :age=>45},
# {:name=>"Jess", :age=>22}]}
# :name_age_node3=>{:name=>"Jess", :age=>22}}}

Nesting a given array into children, based on condition [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 8 years ago.
Improve this question
I have an array like this:
tweets = [
{
:user_id => 234567,
:username => "A",
:created_at => "2012-10-12 10:20:30"
},
{
:user_id => 234568,
:username => "B",
:created_at => "2012-10-12 10:20:34"
},
{
:user_id => 234569,
:username => "C",
:created_at => "2012-10-12 10:20:35"
},
{
:user_id => 234570,
:username => "D",
:created_at => "2012-10-12 10:20:40"
}
]
and another array, like this:
followers = [
{
:user_id => 234567,
:follower_ids => [234568, 56654]
},
{
:user_id => 234568,
:follower_ids => [234569, 454445]
},
{
:user_id => 234569,
:follower_ids => [234570, 56333]
},
{
:user_id => 234570,
:follower_ids => [45566, 61145]
}
]
I want to nest it into a deep structure, where one is made into children of another. For making the children, the condition to be satisfied is:
any other tweet which has a greater created_at than the other, and
whose user_id is included in the follower_ids list if that tweet in
the followers array is considered to be a child
and the expected output for the given data is like this:
arranged_tweets = [
{
:user_id => 234567,
:username => "A",
:created_at => "2012-10-12 10:20:30",
:children => [
{
:user_id => 234568,
:username => "B",
:created_at => "2012-10-12 10:20:34",
:children => [
{
:user_id => 234569,
:username => "C",
:created_at => "2012-10-12 10:20:35",
:children => [
{
:user_id => 234570,
:username => "D",
:created_at => "2012-10-12 10:20:40"
}
]
}
]
}
]
}
]
Untested, but should give you the idea:
arranged_tweets = tweets.collect do |tweet|
arranged_tweet(tweet, tweets - [tweet])
end
def arranged_tweet(tweet, other_tweets)
{ :user_id => tweet[:user_id], ...
:children => children(tweet, other_tweets) }
end
def children(tweet, other_tweets)
other_tweets.find_all { |other| is_child?(other, tweet) }.collect do |other|
arranged_tweet(other, other_tweets - [other])
end
end
def is_child?(tweet, parent_tweet)
parent_tweet[:created_at] > tweet[:created_at] &&
is_follower?(tweet[:user_id], parent_tweet[:user_id])
end
def is_follower?(user_id, other_user_id)
followers[other_user_id][:follower_ids].include?(user_id)
end

turn list of depth first traversal nodes back into tree structure in Ruby

Given the following input (from a CSV file):
input = [
{ :level => 0, :value => "a" },
{ :level => 1, :value => "1" },
{ :level => 1, :value => "2" },
{ :level => 2, :value => "I" },
{ :level => 2, :value => "II" },
{ :level => 2, :value => "III" },
{ :level => 0, :value => "b" },
{ :level => 0, :value => "c" },
{ :level => 0, :value => "d" },
{ :level => 1, :value => "3" },
{ :level => 1, :value => "4" },
]
How can I convert this to the following in "The Ruby Way":
expected = [
{ :value => "a", :children => [ { :value => 1, :children => nil },
{ :value => 2, :children => [ { :value => "I", :children => nil },
{ :value => "II", :children => nil },
{ :value => "III", :children => nil } ] } ] },
{ :value => "b", :children => nil },
{ :value => "c", :children => nil },
{ :value => "d", :children => [ { :value => 3, :children => nil },
{ :value => 4, :children => nil } ] },
]
?
Edited:
My solution to this was to sidestep the problem, transform it and get someone else to solve it:
require 'yaml'
def linear_to_tree(a)
yaml_lines = []
a.each do |el|
indent = " " * 4 * el[:level]
yaml_lines << "#{indent}-"
yaml_lines << "#{indent} :value: #{(el[:value])}"
yaml_lines << "#{indent} :children:"
end
yaml_lines << "" # without this, YAML.load complains
yaml = yaml_lines.join("\n")
# open("test_yaml.txt", "w"){|f| f.write(yaml)}
YAML.load(yaml)
end
But there must be a more elegant way to solve this.
P.S. I'd also like to see a one-liner for this transformation, just to see if it's possible.
You should use an empty array for nodes that have no children, an empty array is the null object for a collection. Otherwise you have to dance around both when you assign it, and when you use it.
def transform(inputs)
transform! inputs.dup
end
def transform!(inputs, output=[], current_level=0)
while inputs.any?
input = inputs.shift
level, value = input.values_at :level, :value
value = value.to_i if value =~ /\A\d+\z/
if level < current_level
inputs.unshift input
break
elsif level == current_level
next_children = []
output << {value: value, children: next_children}
transform! inputs, next_children, current_level.next
else
raise "presumably should not have gotten here"
end
end
output
end

ruby language - merge an array into another by finding same element

A = [
{ :id => 1, :name => 'good', :link => nil },
{ :id => 2, :name => 'bad', :link => nil }
]
B = [
{ :id => 3, :name => 'good' },
{ :id => 4, :name => 'good' },
{ :id => 5, :name => 'bad' }
]
I need to merge array B into A so that :link in array A includes the entry in array B if :name is the same value in each array.
For example, after processing array A should be:
A = [
{ :id => 1, :name => 'good', :link => [{ :id => 3, :name => 'good' }, { :id => 4, :name => 'good' }] },
{ :id => 2, :name => 'bad', :link => [{ :id => 5, :name => 'bad' }] }
]
thanks.
The short version;
a.each { | item | item[:link] = b.find_all { | x | x[:name] == item[:name] } }
Demo here.
In ruby the constants begin with an uppercase letter, so you should use lowercase letter:
A => a, B => b
a.each do |ha|
b.each do |hb|
if ha[:name] == hb[:name]
ha[:link] |= []
ha[:link] << hb
end
end
end
Functional approach:
B_grouped = B.group_by { |h| h[:name] }
A2 = A.map { |h| h.merge(:link => B_grouped[h[:name]]) }
#=> [{:link=>[{:name=>"good", :id=>3}, {:name=>"good", :id=>4}], :name=>"good", :id=>1},
# {:link=>[{:name=>"bad", :id=>5}], :name=>"bad", :id=>2}]

Resources