Merge hashes with multiple keys/values - ruby-on-rails

I have an array of hashes of which I merge hashes that contain specific duplicate key value.
combined_keywords = new_array_of_hashes.each_with_object(Hash.new(0)){|oh, newh|
newh[oh[:keyword]] += oh[:total_value].to_f
}
This creates an array of hashes that look like this:
{ :ACTUAL_KEYWORD => ACTUAL_TOTAL_VALUE }
I'm new to ruby and I don't quite understand the magic behind this. I have an additional key and value to consolidate and now I'm lost. The root of the issue is I don't understand how the consolidation is occurring in this line: newh[oh[:keyword]] += oh[:total_value].to_f
I tried this with no luck:
combined_keywords = new_array_of_hashes.each_with_object(Hash.new(0)){|oh, newh|
newh[oh[:keyword]] += oh[:total_value].to_f
newh[oh[:keyword]] += oh[:revenue_per_transaction].to_f
}
I really just need an array of consildated hashes each similar to:
{ :keyword => "ACTUAL_KEYWORD", :total_value => ACTUAL_TOTAL_VALUE, :revenue_per_transaction => ACTUAL_REVENUE }
Edit:
Input
new_array_of_hashes = [
{ keyword: 'foo', total_value: 1, revenue_per_transaction: 5 },
{ keyword: 'bar', total_value: 2, revenue_per_transaction: 4 },
{ keyword: 'bar', total_value: 4, revenue_per_transaction: 4 },
{ keyword: 'foo', total_value: 3, revenue_per_transaction: 5 },
]
Desired Output
combined_keywords = [
{ keyword: 'foo', total_value: 4, revenue_per_transaction: 10 },
{ keyword: 'bar', total_value: 6, revenue_per_transaction: 8 },
]

Let's say you have:
new_array_of_hashes = [
{ keyword: 'foo', total_value: 1 },
{ keyword: 'bar', total_value: 2 },
{ keyword: 'bar', total_value: 4 },
{ keyword: 'foo', total_value: 3 },
]
Now we'll step through your code:
combined_keywords = new_array_of_hashes.each_with_object(Hash.new(0)){|oh, newh|
newh[oh[:keyword]] += oh[:total_value].to_f
}
This will loop over each hash in the array. We also setup a new hash which returns 0 if we access a key that doesn't exist:
# Pass 1
oh = { keyword: 'foo', total_value: 1 }
newh = {}
newh[ oh[:keyword] ] #=> newh['foo'] This key doesn't exist and returns 0
oh[:total_value].to_f #=> 1.to_f => 1.0
newh[oh[:keyword]] += oh[:total_value].to_f
#=> newh['foo'] = newh['foo'] + oh[:total_value].to_f
#=> newh['foo'] = 0 + 1.0
# Pass 2
oh = { keyword: 'bar', total_value: 2 }
newh = { 'foo' => 1.0 }
newh[ oh[:keyword] ] #=> newh['bar'] This key doesn't exist and returns 0
oh[:total_value].to_f #=> 2.to_f => 2.0
newh[oh[:keyword]] += oh[:total_value].to_f
#=> newh['bar'] = newh['bar'] + oh[:total_value].to_f
#=> newh['bar'] = 0 + 2.0
Now since we have keys for the next two iterations we access things as normal:
# Pass 3
oh = { keyword: 'bar', total_value: 4 }
newh = { 'foo' => 1.0, 'bar' => 2.0 }
newh[ oh[:keyword] ] #=> newh['bar'] This key now exists and returns 2.0
oh[:total_value].to_f #=> 4.to_f => 4.0
newh[oh[:keyword]] += oh[:total_value].to_f
#=> newh['bar'] = newh['bar'] + oh[:total_value].to_f
#=> newh['bar'] = 2.0 + 4.0
# Pass 4
oh = { keyword: 'foo', total_value: 3 }
newh = { 'foo' => 1.0, 'bar' => 6.0 }
newh[ oh[:keyword] ] #=> newh['foo'] This key now exists and returns 1.0
oh[:total_value].to_f #=> 3.to_f => 3.0
newh[oh[:keyword]] += oh[:total_value].to_f
#=> newh['foo'] = newh['foo'] + oh[:total_value].to_f
#=> newh['foo'] = 1.0 + 3.0
When the block returns it will return newh; this is how each_with_object works.
As you can see, what is returned is a hash of the form:
{ 'foo' => 4.0, 'bar' => 6.0 }
So this is only a combined array where the new key was the stored :keyword object, and the value was the sum total.
Based on your new hash form
{
keyword: "ACTUAL_KEYWORD",
total_value: ACTUAL_TOTAL_VALUE,
revenue_per_transaction: ACTUAL_REVENUE
}
This format won't make much sense. Since hashes only have key:value pairs. You may need to have a hash of sub-hashes, or run through the loop twice. Once for :total_value and once for :revenue_per_transaction. It will really depend what you want your final object(s) to be.
Edit:
Based on your new expected input and output, you could use:
sum_keys = [:total_value, :revenue_per_transaction]
new_array_of_hashes.group_by{ |h| h[:keyword] }
.map{ |keyword, related|
tmp = {keyword: keyword}
tmp.merge! Hash[sum_keys.zip Array.new(sum_keys.size, 0)]
related.reduce(tmp){ |summed, h|
sum_keys.each{ |key| summed[key] += h[key] }
summed
}
}
#=> [
# { keyword: 'foo', total_value: 4, revenue_per_transaction: 10 },
# { keyword: 'bar', total_value: 6, revenue_per_transaction: 8 },
#]
It's a bit messy. I'd probably refactor what the map call is doing into it's own helper method. The reason I'm providing a start value to reduce is because otherwise it will mutate the original hash from new_array_of_hashes.

Given
foos = [ { :key => 'Foo', :value => 1, :revenue => 2 },
{ :key => 'Foo', :value => 4, :revenue => 8 } ]
You can do this
foos.each_with_object(Hash.new(0)) do |foo_hash, new_hash|
new_hash[:keyword] = foo_hash[:key]
new_hash[:total_value] += foo_hash[:value]
new_hash[:total_revenue] += foo_hash[:revenue]
end
So each_with_object allows you to pass an argument to an enumerable's .each block. In this case you are passing Hash.new(0). The 0 argument is a way to set a default hash value so you don't have to explicitly zero the value to zero in the loop and just get right to incrementing. The += is just shorthand. So a += b is equivalent to a = a + b.
The clunky thing about the loop is it sets the new_hash[:keyword] value on every pass. You could tack on a if new_hash[:keyword] == 0 (because it initials to zero) but that's just a bandaid. The problem lies in the original hashes structure. If :key aways equals 'Foo' then 'Foo' is superfluous. If it's not always 'Foo' then this loop isn't very useful.
The loop above yields
{ :keyword => 'Foo', :total_value => 5, :total_revenue => 10 }

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

Merge a single array into nested array in rails

I have a list of objects something like this:
a = [
{
id: 0,
name: "ABC",
work: "ABC2"
},
{
id: 0,
name: "XYZ",
work: "XYZ2"
},
{
id: 1,
name: "ACD",
work: "ACD2"
}
]
And I want to convert it into something like this:
b = [
{
id: 0,
fields: [
{
name: "ABC",
work: "ABC2"
},
{
name: "XYZ",
work: "XYZ2"
}
]
},
{
id: 1,
fields: [
{
name: "ACD",
work: "ACD2"
}
]
}
]
The idea is to group the objects (by id) in one array.
The approach I tried is:
b = []
rest_object = []
a.each_with_index do |aa, idx|
aa.delete(:id)
rest_object << aa
if idx == 0
next
end
puts a[idx][:id], a[idx-1][:id]
if a[idx][:id] != a[idx-1][:id]
b << {id: a[idx-1][:id], names: rest_object}
rest_object = []
end
end
But I am getting an empty output.
Also, if it is possible to achieve the same in some cleaner way.
That would be helpful.
Something like this does the job. This deletes the :id key-value pair from each hash and uses the value to group the remainder of the hash. Then map the resulting hash to created an array and transform the data into {id: ..., fields: ...} format.
a = [{id: 0, name: "ABC", work: "ABC2"}, {id: 0, name: "XYZ", work: "XYZ2"}, {id: 1, name: "ACD", work: "ACD2"}]
b = a.group_by { |hash| hash.delete(:id) }
.map { |id, fields| {id: id, fields: fields} }
#=> [{:id=>0, :fields=>[{:name=>"ABC", :work=>"ABC2"}, {:name=>"XYZ", :work=>"XYZ2"}]}, {:id=>1, :fields=>[{:name=>"ACD", :work=>"ACD2"}]}]
Note: This mutates the hashes in the a array. If you don't want this change a.group_by to a.map(&:dup).group_by. Which first duplicates all hashes before doing any mutations.
Try following,
required_keys = a[0].except(:id).keys
b = a.group_by { |x| x[:id] }
b = b.inject([]) do |m,(k,v)|
arr = { id: k }
required_keys.each do |key|
arr[key.to_s.pluralize.to_sym] = v.map { |z| z.slice(key) }
end
m << arr
end
# => [{:id=>0, :names=>[{:name=>"ABC"}, {:name=>"XYZ"}]}, {:id=>1, :names=>[{:name=>"ACD"}]}]
a = [
{
id: 0,
name: "ABC"
},
{
id: 0,
name: "XYZ"
},
{
id: 1,
name: "ACD"
}
]
ary = []
a.each do|val|
ary[val[:id]] = {id: val[:id]} unless ary[val[:id]]
ary[val[:id]][:names] = [] unless ary[val[:id]][:names]
ary[val[:id]][:names].push({name: val[:name]})
end
If I understand, given a more general array like this:
a = [ { id: 0, name: "ABC", etc: "01" },
{ id: 0, name: "XYZ", etc: "02" },
{ id: 1, name: "ACD", etc: "11" } ]
The first idea I came up with is doing something like this:
require 'active_support/inflector' # for using pluralize
res = a.group_by{ |h| h[:id] }.values.map do |ary|
h = Hash.new{ |h,k| h[k] = [] }
ary.each { |hh| hh.each { |k,v| h[k] << v } }
h[:id] = h[:id].first
h.transform_keys do |k|
unless k == :id
k.to_s.pluralize.to_sym
else
k
end
end
end
res #=> [{:id=>0, :names=>["ABC", "XYZ"], :etcs=>["01", "02"]}, {:id=>1, :names=>["ACD"], :etcs=>["11"]}]
This is not exactly the format you require, but to get that format, just change this line
ary.each { |hh| hh.each { |k,v| h[k] << v } }
to
ary.each { |hh| hh.each { |k,v| h[k] << { k => v } } }

How can I sort ruby hash with string key by setting priority to the key elements?

I have a hash with the following values:
hash = {
'red': { something: ''},
'green': { something: '' },
'yellow': { something: ''}
}
What is the simplest way to do something like this:
hash.sort_by_key_with_priority(['green', 'yellow', 'red'])
And return:
hash = {
'green': { something: '' },
'yellow': { something: ''},
'red': { something: ''},
}
If your priority array contains all of the hash keys, you don't need sorting. You can simply take the hash apart and construct a new one.
hash = {
red: { something: ''},
green: { something: '' },
yellow: { something: ''}
}
priority = %i[green yellow red]
hash.slice(*priority)
# => {:green=>{:something=>""}, :yellow=>{:something=>""}, :red=>{:something=>""}}
If there are keys in the hash not included in priority array:
>> h = {red: 512, green: 63, yellow: 99, foo: 42, baz: 'hi'}
=> {:red=>512, :green=>63, :yellow=>99, :foo=>42, :baz=>"hi"}
>> p = %i[green yellow red]
=> [:green, :yellow, :red]
# this puts keys not in array at start
# h.sort_by { |k, v| p.include?(k) ? p.index(k) : -1 }
>> h.sort_by { |k, v| p.index(k) || -1 }
=> [[:foo, 42], [:baz, "hi"], [:green, 63], [:yellow, 99], [:red, 512]]
# this puts keys not in array at end
# h.sort_by { |k, v| p.include?(k) ? p.index(k) : p.size }
>> h.sort_by { |k, v| p.index(k) || p.size }
=> [[:green, 63], [:yellow, 99], [:red, 512], [:foo, 42], [:baz, "hi"]]
Use to_h method on result to convert to hash
hash = {
'red': { something: ''},
'green': { something: '' },
'yellow': { something: ''}
}
Code
p ['green', 'yellow', 'red'].map{|value|[value.to_sym,hash[value.to_sym]]}.to_h
output
{:green=>{:something=>""}, :yellow=>{:something=>""}, :red=>{:something=>""}}
Following will also work,
priority = %i(:green yellow red)
priority.inject({}) { |m, v| m[v] = hash[v]; m }

Ruby merge hash value

I've written an API to organize my daily user data.
The origin format is like
"dau": {
"2017-05-02": 1,
"2017-05-04": 2,
"2017-05-05": 2,
}
"new_user": {
"2017-05-02": 1,
"2017-05-04": 0,
"2017-05-07": 0,
}
It's hard to display in HTML table row by row.
Therefore, I want the format to become this one.
However, I have no idea how to deal with.
info: {
"2017-05-02": {
dau: 1,
new_user: 1
},
"2017-05-04": {
dau: 2,
new_user: 0
},
"2017-05-05": {
dau: 2
},
"2017-05-07": {
new_user: 0
}
}
suppose you have data, then
(data['dau'].keys + data['new_user'].keys).uniq.map { |k| [k, { dau: data['dau'][k].to_i, new_user: data['new_user'][k].to_i } ] }.to_h
and if you don't want default/0 values then,
(data['dau'].keys + data['new_user'].keys).uniq.map { |k| [k, { dau: data['dau'][k], new_user: data['new_user'][k] }.compact ] }.to_h
Output:
{
"2017-05-02" => {
:dau => 1,
:new_user => 1
},
"2017-05-04" => {
:dau => 2,
:new_user => 0
},
"2017-05-05" => {
:dau => 2
},
"2017-05-07" => {
:new_user => 0
}
}
Hope it helps..
keys = (dau.keys + new_user.keys).uniq
# [:"2017-05-02", :"2017-05-04", :"2017-05-05", :"2017-05-07"]
keys.each_with_object({}) do |key, result|
result[key] = {dau: dau[key], new_user: new_user[key] }.compact
end
# {:"2017-05-02"=>{:dau=>1, :new_user=>1},
# :"2017-05-04"=>{:dau=>2, :new_user=>0},
# :"2017-05-05"=>{:dau=>2},
# :"2017-05-07"=>{:new_user=>0}}
Here's an alternative with each_with_object. It should work with any number of keys:
data = {"dau": {
"2017-05-02": 1,
"2017-05-04": 2,
"2017-05-05": 2
},
"new_user": {
"2017-05-02": 1,
"2017-05-04": 0,
"2017-05-07": 0
}
}
info = Hash.new { |h, k| h[k] = {} }
info = data.each_with_object(info) do |(key, sub_h), h|
sub_h.each do |date, i|
h[date][key] = i
end
end
p info
# {:"2017-05-02"=>{:dau=>1, :new_user=>1}, :"2017-05-04"=>{:dau=>2, :new_user=>0}, :"2017-05-05"=>{:dau=>2}, :"2017-05-07"=>{:new_user=>0}}

What is the correct way to chain map_reduce calls in Mongoid?

I have an Element model that belongs to User. I am trying to calculate the following hash: how many users have element count of 1, 2, 3, etc. The approach I take is to first generate a hash of {user -> num elements}, then I sort-of invert it using a second map-reduce.
Here's what I have so far:
Element.map_reduce(%Q{
emit(this.user_id, 1);
}, %Q{
function(key, values) {
return Array.sum(values);
}
}).out(inline: true).map_reduce(%Q{
if (this.value > 1) {
emit(this.value, this._id);
}
}, %Q{
function(element_count, user_ids) {
return user_ids.length;
}
}).out(inline: true)
This gives me an "undefined method `map_reduce'" error. I couldn't find the answer in the docs. Any help would be great.
I calculated the hash using aggregate instead mapreduce, first grouping by user, and then grouping again by elements count:
Element.collection.aggregate([
{
"$group" => {
"_id" => "$user_id", "elements_count" => {"$sum" => 1}
}
},
{
"$group" => {
"_id" => "$elements_count", "users_count" => {"$sum" => 1}
}
},
{ "$project" => {
"_id" => 0,
"users_count" => '$users',
"elements_count" => '$_id',
}
}
])
This returns the following array:
[
{"users_count"=>3, "elements_count"=>2},
{"users_count"=>4, "elements_count"=>3},
...
]
If needed it can also be sorted using $sort operator

Resources