rails grouping an array of hashes - ruby-on-rails

i know this is a simple question but its so frustating for me, i tried many hours and i didnt make it, so i hope can find the answer,
i have array of hashes like this
array = [{date: 1, branch: 1, value: "100"}, {date: 1, branch: 2, value: "200"}, {date: 2, branch: 1, value: "500"}, {date: 3, branch: 2, value: "500"}, {date: 3, branch: 3, value: "300"}]
and i want to grouping it like this
data = [{date: 1, 1: "100", 2: "200"}, {date: 2, 1: "500"}, {date: 3, 2: "500", 3: "300"}]
in the array = [{date: 1, branch: 1, value: "100"}, i want to take the value of branch and value and combine it like this 1: "100"
anyway i can do that, there is no problem if there is using loop or case when method

Probably could be shortened, but it works:
array.group_by { |h| h[:date] }.map do |k, v|
[:date, k, *v.map { |h| [h[:branch], h[:value]] }]
end.map { |x| Hash[*x.flatten] }
# => [{:date=>1, 1=>"100", 2=>"200"}, {:date=>2, 1=>"500"}, {:date=>3, 2=>"500", 3=>"300"}]

Another solution
array = [{date: 1, branch: 1, value: "100"}, {date: 1, branch: 2, value: "200"}, {date: 2, branch: 1, value: "500"}, {date: 3, branch: 2, value: "500"}, {date: 3, branch: 3, value: "300"}]
data = array.inject({}) do |res, val|
if res[val[:date]]
res[val[:date]].merge!({val[:branch] => val[:value]})
else
res.merge!(val[:date] => {val[:branch] => val[:value]})
end
res
end
puts data.collect{|key, val| {date: key}.merge!(val)}

Related

How to find sum on basis of type and name key in ruby? (ruby array of hashes)

How to find a sum on basis of type and name key in ruby? (ruby array of hashes)
eatables = [{type: "fruit", name: "apple", count: 2},
{type: "vegetable", name: "pumpkin", count: 3},
{type: 'fruit', name: 'apple', count: 1},
{type: "vegetable", name: "pumpkin", count: 2}]
Desired Output
[{type: "fruit", name: "apple", count: 3},
{type: "vegetable", name: "pumpkin", count: 5}]
eatables.group_by { |h| h.slice(:name, :type) }
.map { |key, grouped| key.merge(count: grouped.sum{ |h| h[:count] }) }
The first operation splits the array into groups based on the name and type.
{{:name=>"apple", :type=>"fruit"}=>[{:type=>"fruit", :name=>"apple", :count=>2}, {:type=>"fruit", :name=>"apple", :count=>1}], {:name=>"pumpkin", :type=>"vegetable"}=>[{:type=>"vegetable", :name=>"pumpkin", :count=>3}, {:type=>"vegetable", :name=>"pumpkin", :count=>2}]}
We then map across that hash and return an array of hashes with the type, name, and sum which outputs:
=> [{:name=>"apple", :type=>"fruit", :count=>3}, {:name=>"pumpkin", :type=>"vegetable", :count=>5}]
eatables.inject(Hash.new(0)) { |h, item|
h[item.slice(:type, :name)] += item[:count]
h
}.map { |k, v|
{**k, count: v}
}
This type of problem can be solved with a reduce
output = eatables.reduce({}) do |hsh, current|
if hsh.has_key?(current[:type]+current[:name])
hsh[current[:type]+current[:name]][:count] += current[:count]
else
hsh[current[:type]+current[:name]] = current
end
hsh
end.values

Hash with 3 different keys each pointing to an array of instances. How do I sort the arrays by id?

I have a hash like so:
def my_requests
result = {
accepted: [],
rejected: [],
pending: [],
}
self.requests.each do |request|
serialized_request = RequestSerializer.new(request)
if request.accept == nil
result[:pending].push(serialized_request)
elsif request.accept
result[:accepted].push(serialized_request)
else
result[:rejected].push(serialized_request)
end
end
result
end
I will have a logged in user. I am trying to organize the logged in user's availabilities by id.
How do I sort each array by id. I know if this was just an array I can do:
array.sort_by{|request| request.id}
But how do I iterate through each key's array? I've tried multiple different ways and the only one that works is if I end up mapping over the hash and then another loop to sort the requests. But that doesn't return a hash. Is there a way for me to keep the structure and sort it?
The availabilities serializer is below:
class RequestSerializer < ActiveModel::Serializer
attributes :id, :start_time, :payment, :number_of_hours, :availability_id, :date, :name, :accept, :postcode, :phone_number
end
Below is one of the key, value pair outputs.
:rejected=>[#<RequestSerializer:0x00007fa416e168a8 #object=#<Request id: 64, payment: 200, number_of_hours: 20, accept: false, start_time: "2000-01-01 16:20:00", venue_id: 1, availability_id: 4, created_at: "2020-08-30 12:15:04", updated_at: "2020-08-30 12:15:52">, #instance_options={}, #root=nil, #scope=nil>, #<RequestSerializer:0x00007fa416e167b8 #object=#<Request id: 4, payment: 160, number_of_hours: 4, accept: false, start_time: "2000-01-01 16:15:00", venue_id: 2, availability_id: 5, created_at: "2020-06-17 21:19:07", updated_at: "2020-06-17 21:21:32">, #instance_options={}, #root=nil, #scope=nil>, #<RequestSerializer:0x00007fa416e166c8 #object=#<Request id: 71, payment: 100, number_of_hours: 1, accept: false, start_time: "2000-01-01 09:45:00", venue_id: 1, availability_id: 6, created_at: "2020-10-01 08:45:43", updated_at: "2020-10-01 08:46:04">, #instance_options={}, #root=nil, #scope=nil>, #<RequestSerializer:0x00007fa416e16560 #object=#<Request id: 66, payment: 30, number_of_hours: 3, accept: false, start_time: "2000-01-01 16:30:00", venue_id: 1, availability_id: 26, created_at: "2020-08-30 12:31:02", updated_at: "2020-08-30 12:32:10">, #instance_options={}, #root=nil, #scope=nil>, #<RequestSerializer:0x00007fa416e163f8 #object=#<Request id: 68, payment: 20, number_of_hours: 3, accept: false, start_time: "2000-01-01 12:00:00", venue_id: 1, availability_id: 28, created_at: "2020-09-01 08:17:26", updated_at: "2020-09-01 13:09:54">, #instance_options={}, #root=nil, #scope=nil>]
Thanks!
result.transform_values { |array| array.sort_by(&:request_id) }
If the arrays are not not arrays of requests, but RequestSerializer, just call .object on them to get the request to sort by.
result.transform_values do |array|
array.sort_by { |serializer| serializer.object.request_id }
end
Another option would be to define request_id on RequestSerializer
You have to sort each hash value separately:
result.each_value { |array| array.sort_by!(&:id) }
Hash#each_value traverses the values and sort_by! sorts the array in-place.
If you need to create a new sorted copy:
result.each_with_object({}) do |(key, value), list|
list[key] = value.sort_by(&:id)
end
As Stefan posted in the comments. I can use a database query to order the requests prior to splitting them into their arrays.
The answer that worked (without doing multiple loops) was:
def my_requests
result = {
accepted: [],
rejected: [],
pending: [],
}
requests.order(:id).each do |request|
serialized_request = RequestSerializer.new(request)
if request.accept == nil
result[:pending].push(serialized_request)
elsif request.accept
result[:accepted].push(serialized_request)
else
result[:rejected].push(serialized_request)
end
end
result
end
Simply by removing self. and using the .order query and the id attribute, everything comes out ordered!
Thanks Stefan! (and everyone else)
(Others worked with multiple methods or loops but as I was using Rails, the query is the quickest and easiest).

Rails: How to merge two hashes if a specific key has the same value?

I'm trying to merge hashes if a specific key has the same value.
here is the array
[{
id: 77,
member_phone: "9876543210",
created_at: "2017-05-03T11:06:03.000Z",
name: "Sure"
},
{
id: 77,
member_phone: "123456789",
created_at: "2017-05-03T11:06:03.000Z",
name: "Sure"
},
{
id: 78,
member_phone: "12345",
created_at: "2017-05-03T11:06:03.000Z",
name: "XYZ"
}]
and the required output:
[{
id: 77,
member_phone: "123456789,9876543210",
created_at: "2017-05-03T11:06:03.000Z",
name: "Sure"
},
{
id: 78,
member_phone: "12345",
created_at: "2017-05-03T11:06:03.000Z",
name: "XYZ"
}]
here's the code I tried:
merge_users.group_by { |h1| h1["id"] }.map do |k,v|
{ "id" => k, :member_phone => v.map { |h2| h2[:member_phone] }.join(", ") }
end
how can I do it?
The following code would work for your given example.
code
result = arr.group_by {|h| h[:id]}.values.map do |arr|
arr.reduce do |h1, h2|
h1.merge(h2) do |k, ov, nv|
ov.eql?(nv) ? ov : [ov, nv].join(",")
end
end
end
p result
#=>[{:id=>77, :member_phone=>"9876543210,123456789", :created_at=>"2017-05-03T11:06:03.000Z", :name=>"Sure"}, {:id=>78, :member_phone=>"12345", :created_at=>"2017-05-03T11:06:03.000Z", :name=>"XYZ"}]
How about:
grouped = data.group_by do |item|
item[:id]
end
combined = grouped.map do |_id, hashes|
hashes.inject({}) do |memo, hash|
memo.merge(hash)
end
end
It works in two passes:
First group all hashes by the value of the :id key
This returns a Hash with the id as key, and an array (of all the hashes with this id) as value.
In a second pass all the hashes are merged and mapped to an array again.
arr = [
{ id: 77, phone: "9876543210", name: "Sure" },
{ id: 77, phone: "123456789", name: "Sure" },
{ id: 78, phone: "12345", name: "XYZ" }
]
You could use the form of Hash#update (aka merge!) that uses a block to compute the values of keys that are present in both hashes being merged.
arr.each_with_object({}) { |g,h| h.update(g[:id]=>g) { |_,o,n|
o.merge(phone: "#{o[:phone]}#{n[:phone]}") } }.values
#=> [{:id=>77, :phone=>"9876543210123456789", :name=>"Sure"},
# {:id=>78, :phone=>"12345", :name=>"XYZ"}]
Note that the receiver of Hash#values is the following.
#=> {77=>{:id=>77, :phone=>"9876543210123456789", :name=>"Sure"},
# 78=>{:id=>78, :phone=>"12345", :name=>"XYZ"}}
See the doc for Hash#update for definitions of the three block variables _, o and n. I used an underscore for the first variable (a valid name for a local variable) to signify that it is not used in the block calculation (a common practice).
Note that Hash#update can almost always be used when Enumerable#group_by can be used, and vice-versa.
Here's one way to use Hash#group_by here.
arr.group_by { |h| h[:id] }.
map { |_,a| a.first.merge(phone: a.map { |h| h[:phone] }.join) }
#=> [{:id=>77, :phone=>"9876543210123456789", :name=>"Sure"},
# {:id=>78, :phone=>"12345", :name=>"XYZ"}]
Note that
arr.group_by { |h| h[:id] }
#=> {77=>[{:id=>77, :phone=>"9876543210", :name=>"Sure"},
# {:id=>77, :phone=>"123456789", :name=>"Sure"}],
# 78=>[{:id=>78, :phone=>"12345", :name=>"XYZ"}]}

Ruby: how to perform "left join" on two arrays of hashes efficiently

I am trying to left join the following arrays of hashes:
input:
a = [{id: 1, name: 'Bob'}, {id: 2, name: 'Jack'}, {id: 3, name: 'Tom'}]
b = [{id: 3, age: 12}, {id: 2, age: 7}]
output:
[{id: 1, name: 'Bob', age: nil}, {id: 2, name: 'Jack', age: 7}, {id: 3, name: 'Tom', age: 12}]
Currently I am doing something along the lines with:
a.map do |x|
{
id: x[:id],
name: x[:name],
age: (b.detect{|y| x[:id] == y[:id]} || {age: nil}).fetch(:age)
}
end
It works, but it is super slow when the data set is large.
Is there any better way to perform the "join" operation more efficiently?
[a, b].map { |a| a.group_by { |e| e[:id] } }
.reduce do |a, b|
a.merge(b) { |_, v1, v2| v1.first.merge v2.first }
end.values
.map do |e|
Array === e ? {age:nil, name:nil}.merge(e.first) : e
end
The whole preparation step takes O(N) and the merge then is done as O(N), plus the finalization takes O(N).
h = b.each_with_object({}) { |g,h| h[g[:id]] = g[:age] }
#=> {3=>12, 2=>7}
a.map { |g| g.merge(age: h[g[:id]]) }
#=> [{:id=>1, :name=>"Bob", :age=>nil},
# {:id=>2, :name=>"Jack", :age=>7},
# {:id=>3, :name=>"Tom", :age=>12}]
If a is to be modified in place, change the second line to
a.each { |g| g[:age] = h[g[:id]] }
a #=> [{:id=>1, :name=>"Bob", :age=>nil},
# {:id=>2, :name=>"Jack", :age=>7},
# {:id=>3, :name=>"Tom", :age=>12}]

Most efficient way to convert a ruby hash with array as keys to one with single values as keys

I'm stuck on what is the best way to re-arrange my ruby hash.
The main goal is to group results from mysql by month and count.
To do it, i make this request:
#data = Model.find(params[:id])
.jobs
.group('year(created_at)')
.group('month(created_at)')
.count(:id)
Which gives me:
#=> {[2013, 12]=>9, [2014, 1]=>4, [2014, 3]=>3,
# [2014, 4]=>1, [2014, 6]=>1, [2014, 7]=>1, [2014, 10]=>2}
I'm trying to have a cleaner hash or array to convert to json, where the years are not duplicated.
How can I have something workable like {"Year" => [Month,Count value]}? (or other form)
Idea? (I run ruby 2.2)
The answer proposed by #mudosobwa might be correct, but because your target is a json file, you may want some 'named' keys. I suggest you this one :
formated_results = #data.group_by{|k, v| k[0]}.collect{|k,v| {year: k, datas: v.collect{|vv| {month: vv.first.last, count: vv.last}}}}
# {:year=>2013, :datas=>[{:month=>12, :count=>9}]}
# {:year=>2014, :datas=>[{:month=>1, :count=>4}, {:month=>3, :count=>3}, {:month=>4, :count=>1}, {:month=>6, :count=>1}, {:month=>7, :count=>1}, {:month=>10, :count=>2}]}
EDIT : An other solution without the group_by method :
formated_results = Hash.new{|h,k| h[k] = []}
#data.each{|k,v| formated_results[k[0]] << {month: k[1], count: v}}
formated_results = formated_results.collect{|k, v| {year: k, datas: v}}
# {:year=>2013, :datas=>[{:month=>12, :count=>9}]}
# {:year=>2014, :datas=>[{:month=>1, :count=>4}, {:month=>3, :count=>3}, {:month=>4, :count=>1}, {:month=>6, :count=>1}, {:month=>7, :count=>1}, {:month=>10, :count=>2}]}
Then you just have to
formated_results.to_json
The json result shall be :
[
{
"year": 2013,
"datas": [
{ "month": 12, "count": 9 }
]
},
{
"year": 2014,
"datas": [
{ "month": 1, "count": 4 },
{ "month": 3, "count": 3 },
{ "month": 4, "count": 1 },
{ "month": 6, "count": 1 },
{ "month": 7, "count": 1 },
{ "month": 10, "count": 2 }
]
}
]
Given you already have your hash in #data, you might:
#data.inject({}) do |memo, ((y, m), cnt)|
(memo[y] ||= {})[m] = cnt
memo
end
#⇒ {
# 2013 => {12 => 9},
# 2014 => {1 => 4, 10 => 2, 3 => 3, 4 => 1, 6 => 1, 7 => 1}
# }
As it was noted by #Surya in comments, it should be hash Year => Hash[Month, Count]. While you still want to have arrays of Month, Count:
#data.inject({}) do |memo, ((y, m), cnt)|
memo[y] ||= []
memo[y] << [m, cnt]
memo
end
To json:
require 'json'
result.to_json
#=> "{\"2013\":{\"12\":9},\"2014\":{\"1\":4,\"3\":3,\"4\":1,\"6\":1,\"7\":1,\"10\":2}}"
#data.to_a.group_by{|ym, c| ym.first }.map{|year, months| [year,months.map{|m,cnt| [m.last, cnt] }] }.to_h
=> {2013=>[[12, 9]], 2014=>[[1, 4], [3, 3], [4, 1], [6, 1], [7, 1], [10, 2]]}

Resources