Ruby transformation to merge an Array of Hashes into another Array of Hash? - ruby

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

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

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

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

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