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

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}]

Related

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}}}

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

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"
}
]
}

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 transformation to merge an Array of Hashes into another Array of Hash?

I have
[
{:date => "2012-05", :post => 1},
{:date => "2012-12", :post => 1},
{:date => "2013-02", :post => 1},
{:date => "2012-05", :online => 1}
]
And I want to get:
[
{:date => "2012-05", :post => 1, :online => 1},
{:date => "2012-12", :post => 1 },
{:date => "2013-02", :post => 1 }
]
Anyone sees how to apply Ruby hash/array methods to achieve this?
q.group_by { |x| x[:date] }.values.map { |e| e.reduce :merge }
Functional approach:
items_by_date = items.group_by { |h| h[:date] }
result = items_by_date.map { |date, hs| hs.reduce(:merge) }
You can solve it via inject and detect:
arr = [
{:date => "2012-05", :post => 1},
{:date => "2012-12", :post => 1},
{:date => "2013-02", :post => 1},
{:date => "2012-05", :online => 1}
]
arr.inject([]) do |new_array, a|
# if there is an existing hash in the new array with the same date
# merge the values
#
if existing = new_array.detect{ |b| a[:date] == b[:date] }
existing.merge!(a)
else
new_array << a
end
# always return the new array for new iteration
#
new_array
end
Here is one attempt:
a = {:date=>"2012-05", :post=>1}, {:date=>"2012-12", :post=>1}, {:date=>"2013-02", :post=>1}
b = {:date=>"2012-05", :online=>1}
ar = {}; [a, b].flatten.each do |k|
c = k.first[1]; ar[c] ||= Array.new
ar[c] << { k.to_a.last[0] => k.to_a.last[1] }
end
ar.map { |k,v| { k => v[1] ? v[0].merge(v[1]) : v[0] } }
hash = [
{:date => "2012-05", :post => 1},
{:date => "2012-12", :post => 1},
{:date => "2013-02", :post => 1},
{:date => "2012-05", :online => 1}
]
hash.group_by{ |h| h[:date] }.values.map{ |x| x.reduce(:merge) }
=> [{:date=>"2012-05", :post=>1, :online=>1},
{:date=>"2012-12", :post=>1},
{:date=>"2013-02", :post=>1}]

How to remove a record if it is duplicate and sum values :val

Given the following array of hashes:
list=[
{:cod => "0001", :name => "name1", :val => 10},
{:cod => "0001", :name => "name1", :val => 12},
{:cod => "0002", :name => "name2", :val => 13},
{:cod => "0002", :name => "name2", :val => 14},
{:cod => "0002", :name => "name2", :val => 14},
{:cod => "0004", :name => "name4", :val => 16},
{:cod => "0004", :name => "name4", :val => 16},
{:cod => "0004", :name => "name4", :val => 17},
{:cod => "0005", :name => "name5", :val => 17},
{:cod => "0005", :name => "name5", :val => 17},
{:cod => "0005", :name => "name5", :val => 17},
{:cod => "0006", :name => "name6", :val => 110},
{:cod => "0006", :name => "name6", :val => 10},
]
How can I remove duplicate records?
Also, how can I find the sum of the values with the key :val?
You can pass a block to the method uniq of Array to determine the uniqueness.
list.uniq { |h| h[:val] }
=> [{:cod=>"0001", :name=>"name1", :val=>10},
{:cod=>"0001", :name=>"name1", :val=>12},
{:cod=>"0002", :name=>"name2", :val=>13},
{:cod=>"0002", :name=>"name2", :val=>14},
{:cod=>"0004", :name=>"name4", :val=>16},
{:cod=>"0004", :name=>"name4", :val=>17},
{:cod=>"0006", :name=>"name6", :val=>110}]
list.map do |a|
list.select { |b| b[:cod] == a[:cod] && b[:name] == a[:name] } \
.reduce { |res, c| {:cod => c[:cod], :name => c[:name], :val => ((res[:val] + c[:val]) || c[:val])} }
end.uniq { |h| h[:cod]}.each {|c| puts c.inspect}
output:
{:name=>"name1", :cod=>"0001", :val=>22}
{:name=>"name2", :cod=>"0002", :val=>41}
{:name=>"name4", :cod=>"0004", :val=>49}
{:name=>"name5", :cod=>"0005", :val=>51}
{:name=>"name6", :cod=>"0006", :val=>120}
Use group_by:
list.group_by{|x| x[:cod]}.map{|k, v| v[0].merge({:val => v.map{|x| x[:val]}.reduce(:+)})}
Based on the answers so far, there's some confusion about what you actually mean by "remove duplicate records." My interpretation of what you mean is that you wish to only remove records that are exact duplicates. If so, then it is much simpler than the other solutions presented:
list.uniq
This returns:
[{:cod=>"0001", :name=>"name1", :val=>10},
{:cod=>"0001", :name=>"name1", :val=>12},
{:cod=>"0002", :name=>"name2", :val=>13},
{:cod=>"0002", :name=>"name2", :val=>14},
{:cod=>"0004", :name=>"name4", :val=>16},
{:cod=>"0004", :name=>"name4", :val=>17},
{:cod=>"0005", :name=>"name5", :val=>17},
{:cod=>"0006", :name=>"name6", :val=>110},
{:cod=>"0006", :name=>"name6", :val=>10}]
If you want the sum of the :val fields of the unique records, you can do this:
list.uniq.map{|h| h[:val]}.reduce(:+)
That grabs the unique elements (as above), then grabs the :val value from each, and finally applies :+ (addition) to them to get the sum.
list.uniq.group_by { |e| [e[:cod], e[:name]] }.map do |k, v|
{k => v.map { |h| h[:val] }.reduce(:+)}
end
=> [{["0001", "name1"]=>22}, {["0002", "name2"]=>27}, {["0004", "name4"]=>33}, {["0005", "name5"]=>17}, {["0006", "name6"]=>120}]

Resources