Related
Greetings fellow fans of MongoDB!
I've got here a data structure with board game data where achieved scores (after every round) are tracked as nested arrays associated with the player's name. Note that with each board game there's a different set of players:
{
"BoardGames" : {
"Game1" : {
"players" : {
"Anne" : [97, 165, 101, 67],
"Pete" : [86, 115, 134, 149],
"Scott" : [66, 89, 103, 74],
"Jane" : [113, 144, 125, 99],
"Katie" : [127, 108, 98, 151]
}
},
"Game2" : {
"players" : {
"Margot" : [1, 0, 0, 0],
"Pete" : [0, 0, 1, 1],
"Michael" : [0, 0, 0, 0],
"Jane" : [0, 0, 1, 0]
}
},
"Game3" : {
"players" : {
"Chris" : [6, 2, 4, 0, 5, 7],
"Pete" : [4, 5, 2, 5, 3, 1, 4],
"Julia" : [3, 7, 4, 0],
"Tom" : [3, 2, 4, 8, 2, 6, 7]
}
},
}
Game1: Players earn as many victory points per round as they can
Game2: Winning around earns 1, losing a round 0
Game3: Players may leave after every round, hence some players have played more rounds than others, so these arrays are different in their length
So, here are my questions:
Which player got the most points in each game? Who the least?
Who is the winner in the first round? 2nd round, etc.
Who is sitting on 1st, 2nd and 3rd rank from all played games?
I've done quite some queries with mongo, but so far with a nested array that is attached to a flexible/unpredictable parent node I have no idea how to write a query. Also, maybe this is not the best way how I structured the data. So in case you have a better idea, I'd be happy to learn!
Cheers!
P.S.: The insertMany statement to above JSON data:
db.boardGames.insertMany([
{
"_id" : 1,
"Game1" : {
"players" : {
"Anne" : [97, 165, 101, 67],
"Pete" : [86, 115, 134, 149],
"Scott" : [66, 89, 103, 74],
"Jane" : [113, 144, 125, 99],
"Katie" : [127, 108, 98, 151]
}
},
"Game2" : {
"players" : {
"Margot" : [1, 0, 0, 0],
"Pete" : [0, 0, 1, 1],
"Michael" : [0, 0, 0, 0],
"Jane" : [0, 0, 1, 0]
}
},
"Game3" : {
"players" : {
"Chris" : [6, 2, 4, 0, 5, 7],
"Pete" : [4, 5, 2, 5, 3, 1, 4],
"Julia" : [3, 7, 4, 0],
"Tom" : [3, 2, 4, 8, 2, 6, 7]
}
}
}]);
the schema you have is not ideal. if it was something like this: https://mongoplayground.net/p/o8m205t9UKG then you can query like the following:
find winner of each game:
db.collection.aggregate(
[
{
$set: {
Players: {
$map: {
input: "$Players",
as: "x",
in: {
Name: "$$x.Name",
TotalScore: { $sum: "$$x.Scores" }
}
}
}
}
},
{
$unwind: "$Players"
},
{
$sort: { "Players.TotalScore": -1 }
},
{
$group: {
_id: "$Name",
Winner: { $first: "$Players.Name" }
}
}
])
find top 3 ranking players across all games:
db.collection.aggregate(
[
{
$set: {
Players: {
$map: {
input: "$Players",
as: "x",
in: {
Name: "$$x.Name",
TotalScore: { $sum: "$$x.Scores" }
}
}
}
}
},
{
$unwind: "$Players"
},
{
$group: {
_id: "$Players.Name",
TotalScore: { $sum: "$Players.TotalScore" }
}
},
{
$sort: { TotalScore: -1 }
},
{
$limit: 3
},
{
$group: {
_id: null,
TopRanks: { $push: "$_id" }
}
},
{
$project: {
_id: 0,
TopRanks: 1
}
}
])
find the winner of each round across all games
db.collection.aggregate(
[
{
$set: {
"Players": {
$map: {
input: "$Players",
as: "p",
in: {
Scores: {
$map: {
input: "$$p.Scores",
as: "s",
in: {
Player: "$$p.Name",
Round: { $add: [{ $indexOfArray: ["$$p.Scores", "$$s"] }, 1] },
Score: "$$s"
}
}
}
}
}
}
}
},
{
$unwind: "$Players"
},
{
$unwind: "$Players.Scores"
},
{
$replaceRoot: { newRoot: "$Players.Scores" }
},
{
$sort: {
Round: 1,
Score: -1
}
},
{
$group: {
_id: "$Round",
Winner: { $first: "$Player" }
}
},
{
$project: {
_id: 0,
Round: "$_id",
Winner: 1
}
},
{
$sort: {
Round: 1
}
}
])
I have this array right here and I need to get the "id" of each object
[{ id: 1, points: 60 }, { id: 2, points: 20 }, { id: 3, points: 95 }, { id: 4, points: 75 }]
customers = [{ id: 1, points: 90 }, { id: 2, points: 20 }, { id: 3, points: 70 }, { id: 4, points: 40 }, { id: 5, points: 60 }, { id: 6, points: 10}]
I know how to go through the whole array with
#scores.each_with_index{ |score, index| }
However, I haven't found a way to get the objects's points.
Perhaps you are looking for the following.
customers = [
{ id: 1, points: 90 }, { id: 2, points: 20 },
{ id: 3, points: 70 }, { id: 4, points: 40 },
{ id: 5, points: 60 }, { id: 6, points: 10}
]
h = customers.each_with_object({}) do |g,h|
id, points = g.values_at(:id, :points)
h[id] = points
end
#=> {1=>90, 2=>20, 3=>70, 4=>40, 5=>60, 6=>10}
This allows you to easily extract information of interest, such as the following.
h.keys
#=> [1, 2, 3, 4, 5, 6]
h.values
#=> [90, 20, 70, 40, 60, 10]
h[2]
#=> 20
h.key?(5)
#=> true
h.key?(7)
#=> false
h.value?(70)
#=> true
h.value?(30)
#=> false
What you called score is actually an hash like { id: 1, points: 60 } and I'm going to call it item
So, let's try
#scores.each_with_index do |item, index|
puts "#{index + 1}: id #{item[:id]}, points #{item[:points]}"
end
So, I have this array right here and I need to get the id of each object
In order to transform each element of a collection, you can use Enumerable#map (or in this case more precisely Array#map):
customers.map { _1[:id] }
#=> [1, 2, 3, 4, 5, 6]
This given construct is an array of objects so we need to individually iterate through each element and print out the value present in the objects. The following code shows how we can do it:
customers.each{|obj| p obj[:id].to_s+" "+ obj[:points].to_s }
Here we iterate through each element and print out individual entities of the hash using the obj[:id]/obj[:points] (obj being each individual object here.)
What about something like this?
customers.map(&:to_proc).map{ |p| [:id, :points].map(&p) }
=> [[1, 90], [2, 20], [3, 70], [4, 40], [5, 60], [6, 10]]
Dears,
I really am not able to achieve following result:
{"2002":[1,2,3...]},
{"2003":[1,2,3,4...]},
...
I have following data (short example below):
{ "Year": "28-01-2020", "numbers": [10, 12, 20, 32, 35, 37] },
{ "Year": "03-10-2019", "numbers": [1, 6, 16, 19, 20, 30] },
{ "Year": "11-01-2018", "numbers": [14, 21, 25, 27, 30, 39] },
{ "Year": "11-08-2015", "numbers": [8, 16, 17, 18, 38, 46] },
I managed to use lodash _.groupBy to achieve following mid result:
[{…}]
0:
2000: Array(4)
0:
Year: "2000"
numbers: (7) [empty, 6, 25, 27, 37, 48, 49]
Year: "2000"
numbers: (7) [empty, 7, 12, 19, 30, 45, 49]
2: {Year: "2000", numbers: Array(7)}
2001: Array(104)
[0 … 99]
[100 … 103]
100: {Year: "2001", numbers: Array(7)}
101: {Year: "2001", numbers: Array(7)}
[0 … 99]
0: {Year: "2002", numbers: Array(7)}
1: {Year: "2002", numbers: Array(7)}
..
but i would like to have one object per year, with all numbers that appeared in sub arrays in this year
Could you please help me, i have tired ES6 map and for in loops, but non give me the proper result
Thank you in advance
This worked for me:
const slicedYear = results.map(elem => (elem = { Year: elem.Year.slice(elem.Year.length - 4), numbers: elem.numbers }));
const groupedResults = _.groupBy(slicedYear, 'Year');
for (let key in groupedResults) {
const selectedNumbers = { [key]: [groupedResults[key].map(elem => elem.numbers)].flat(2) };
}
Removing nils is quite simple, however, I'd like to know:
1) What am I doing wrong, why does my array result below include nils?
2) How I can PREVENT the nils from being added to my array, instead of removing them after the fact.
#cars = Array.new
plucked_array = [
[8, "Chevy", "Camaro", 20],
[9, "Ford", "Mustang", 55],
[9, "Ford", "Fusion", 150]
]
plucked_array.
each { |id, make, model, model_count|
#cars[id] ||= {name: make, id: make, data: []}
#cars[id][:data].push([model, model_count])
}
puts #cars.inspect
#=>[nil, nil, nil, nil, nil, nil, nil, nil, {:name=>"Chevy", :id=>"Chevy", :data=>[["Camaro", 20]]}, {:name=>"Ford", :id=>"Ford", :data=>[["Mustang", 55], ["Fusion", 150]]}]
puts #cars.compact.inspect
#=>[{:name=>"Chevy", :id=>"Chevy", :data=>[["Camaro", 20]]}, {:name=>"Ford", :id=>"Ford", :data=>[["Mustang", 55], ["Fusion", 150]]}]
# This gives the result I'm looking for,
# just wondering how to best get transformed array without the post-cleanup.
I also attempted #theTinMan's recommendation to select first, then map, but I had the same results:
plucked_array.select { |id, make, model, model_count|
#cars[id] = {'name' => make, 'id' => make, 'data' => []}
}.map { |id, make, model, model_count|
#cars[id]['data'].push([model, model_count])
}
puts #cars.inspect
#=>[nil, nil, nil, nil, nil, nil, nil, nil, {:name=>"Chevy", :id=>"Chevy", :data=>[["Camaro", 20]]}, {:name=>"Ford", :id=>"Ford", :data=>[["Mustang", 55], ["Fusion", 150]]}]
I've tried, with partial success, to use Hash instead of an array for #cars. This prevented nils, but my end goal is to build "drilldown: series[]" below, which is an array of hashes:
// Create the chart
Highcharts.chart('container', {
chart: {
type: 'column'
},
title: {
text: 'Imaginary Car Stats'
},
subtitle: {
text: 'Click the columns to view models.'
},
xAxis: {
type: 'category'
},
yAxis: {
title: {
text: 'Total car count'
}
},
legend: {
enabled: false
},
plotOptions: {
series: {
borderWidth: 0,
dataLabels: {
enabled: true,
format: '{point.y}'
}
}
},
tooltip: {
headerFormat: '<span style="font-size:11px">{series.name}</span><br>',
pointFormat: '<span style="color:{point.color}">{point.name}</span>: <b>{point.y:.2f}%</b> of total<br/>'
},
/*I have separate `pluck` query for this top-level series:*/
"series": [
{
"name": "Cars",
"colorByPoint": true,
"data": [
{
"name": "Ford",
"y": 205,
"drilldown": "Ford"
},
{
"name": "Chevy",
"y": 20,
"drilldown": "Chevy"
},
{
"name": "Other",
"y": 16,
"drilldown": null
}
]
}
],
"drilldown": {
/*This is the array of hashes I'm attempting to create:*/
"series": [
{
"name": "Ford",
"id": "Ford",
"data": [
[
"Fusion",
150
],
[
"Mustang",
55
]
]
},
{
"name": "Chevy",
"id": "Chevy",
"data": [
[
"Camaro",
20
]
]
}
]
}
});
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/data.js"></script>
<script src="https://code.highcharts.com/modules/drilldown.js"></script>
<div id="container" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
The reason your code is behaving the way it does is because array[8] = x inserts x at position 8 in the array, and ruby fills the spaces up to 8 with nils.
a = []
a[7] = 4
a == [nil, nil, nil, nil, nil, nil, nil, 4]
You need #cars to be a hash - not an array
I think this will do what you are trying to do:
plucked_array = [
[8, "Chevy", "Camaro", 20],
[9, "Ford", "Mustang", 55],
[9, "Ford", "Fusion", 150]
]
cars = plucked_array.each_with_object({}) do |(id, make, model, count), cars|
cars[id] ||= {id: id, make: make, data: []}
cars[id][:data] << [model, count]
end
p cars.values
Which actually is almost identical to #Austio's solution.
I would go for more of a reduce approach because you are taking a list and boiling it down into another object. each_with_object does the reduce but implicitly returns the obj (in this case cars) in every loop
new_list = plucked_array.each_with_object({}) do |(id, make, model, model_count), cars|
# return before mutating cars hash if the car info is invalid
cars[id] ||= {name: make, id: make, data: []}
cars[id][:data].push([model, model_count])
end
# Then in your controller to handle the usage as an array
#cars = new_list.values
Side note: map is normally more for transforms or equivalent changes, which is i think why it feels off here.
I suggest grouping by id, then creating a hash:
plucked_array.group_by(&:first).transform_values{ |v| v.map{ |id, make, model, model_count| {name: make, id: make, data: [model, model_count]} }}
# {8=>[{:name=>"Chevy", :id=>"Chevy", :data=>["Camaro ls", 20]}],
# 9=>[{:name=>"Ford", :id=>"Ford", :data=>["Mustang", 55]}, {:name=>"Ford", :id=>"Ford", :data=>["Fusion", 150]}]}
# }
Edit - update do match the edit of the original question (get series of data for Highcharts)
This should return the required result:
plucked_array.map.with_object(Hash.new([])) { |(id, make, model, model_count), h|
h[make] += [[model, model_count]] }.map { |k, v| {name: k, id: k, data: v }}
#=> [{:name=>"Chevy", :id=>"Chevy", :data=>[["Camaro", 20]]}, {:name=>"Ford", :id=>"Ford", :data=>[["Mustang", 55], ["Fusion", 150]]}]
The first part builds an hash like this.
#=> {"Chevy"=>[["Camaro", 20]], "Ford"=>[["Mustang", 55], ["Fusion", 150]]}
Passing Hash.new([]) as object allows to insert elements to the default array.
Finally the hash is mapped to the required keys.
I have a ruby array of 3 hashes. Each peace has information about report_data (consumption of 2 types of energy) and monthes_data (same for each). Please see the code below.
arr = [{:report_data=>
[{:type=>
{"id"=>1, "name"=>"electricity"},
:data=>[10, 20, 30, 40]},
{:type=>
{"id"=>2, "name"=>"water"},
:data=>[20, 30, 40, 50]}],
:monthes_data=>
{:monthes=>
["jan", "feb"]},
{:report_data=>
[{:type=>
{"id"=>1, "name"=>"electricity"},
:data=>[15, 25, 35, 45]},
{:type=>
{"id"=>2, "name"=>"water"},
:data=>[25, 35, 45, 55]}],
:monthes_data=>
{:monthes=>
["jan", "feb"]},
{:report_data=>
[{:type=>
{"id"=>1, "name"=>"electricity"},
:data=>[17, 27, 37, 47]},
{:type=>
{"id"=>2, "name"=>"water"},
:data=>[27, 37, 47, 57]}],
:monthes_data=>
{:monthes=>
["jan", "feb"]}]
I'm new to Ruby. Please help me to sum all the data by energy types. In the end I want to have one hash with report_data and monthes_data. I need the result look like:
{:report_data=>
[{:type=>
{:"id"=>1, "name"=>"electricity"},
:data=>[42, 72, 102, 132]},
{:type=>
{"id"=>2, "name"=>"water"}},
:data=>[72, 102, 132, 162]}],
:monthes_data=>
{:monthes=>
["jan", "feb"]}}
arr = [{:report_data=>
[{:type=>
{"id"=>1, "name"=>"electricity"},
:data=>[10, 20, 30, 40]},
{:type=>
{"id"=>2, "name"=>"water"},
:data=>[20, 30, 40, 50]}],
:monthes_data=>
{:monthes=>
["jan", "feb"]}},
{:report_data=>
[{:type=>
{"id"=>1, "name"=>"electricity"},
:data=>[15, 25, 35, 45]},
{:type=>
{"id"=>2, "name"=>"water"},
:data=>[25, 35, 45, 55]}],
:monthes_data=>
{:monthes=>
["jan", "feb"]}},
{:report_data=>
[{:type=>
{"id"=>1, "name"=>"electricity"},
:data=>[17, 27, 37, 47]},
{:type=>
{"id"=>2, "name"=>"water"},
:data=>[27, 37, 47, 57]}],
:monthes_data=>
{:monthes=>
["jan", "feb"]}}]
acc = {}
arr.each do
|e| e[:report_data].each_with_index do
|e, idx|
type = e[:type]['id']
e[:data].each_with_index do
|e, idx|
acc[type] = [] if not acc[type]
acc[type][idx] = (acc[type][idx] or 0) + e
end
end
end
p acc
outputs
{1=>[42, 72, 102, 132], 2=>[72, 102, 132, 162]}
You should be able to reformat this into your record
Code
def convert(arr)
{ :months_data=>arr.first[:months_data],
:report_data=>arr.map { |h| h[:report_data] }.
transpose.
map { |d| { :type=>d.first[:type] }.
merge(:data=>d.map { |g| g[:data] }.transpose.map { |a| a.reduce(:+) }) }
}
end
Example
Half the battle in problems such as this one is visualizing the data. It's much clearer, imo, when written like this:
arr = [
{:report_data=>[
{:type=>{"id"=>1, "name"=>"electricity"}, :data=>[10, 20, 30, 40]},
{:type=>{"id"=>2, "name"=>"water"}, :data=>[20, 30, 40, 50]}
],
:months_data=>{:months=>["jan", "feb"]}
},
{:report_data=>[
{:type=>{"id"=>1, "name"=>"electricity"}, :data=>[15, 25, 35, 45]},
{:type=>{"id"=>2, "name"=>"water"}, :data=>[25, 35, 45, 55]}
],
:months_data=>{:months=>["jan", "feb"]}
},
{:report_data=>[
{:type=>{"id"=>1, "name"=>"electricity"}, :data=>[17, 27, 37, 47]},
{:type=>{"id"=>2, "name"=>"water"}, :data=>[27, 37, 47, 57]}],
:months_data=>{:months=>["jan", "feb"]}
}
]
Let's try it:
convert(arr)
#=> {:months_data=>{:months=>["jan", "feb"]},
# :report_data=>[
# {:type=>{"id"=>1, "name"=>"electricity"}, :data=>[42, 72, 102, 132]},
# {:type=>{"id"=>2, "name"=>"water"}, :data=>[72, 102, 132, 162]}
# ]
# }
Explanation
The first thing I did is concentrate on computing the sums, so I converted this to the values of :report_data. That key, and the key-value pair of months' data, which is the same for all elements (hashes) of arr, can be added back in later.
b = arr.map { |h| h[:report_data] }
#=> [
# [{:type=>{"id"=>1, "name"=>"electricity"}, :data=>[10, 20, 30, 40]},
# {:type=>{"id"=>2, "name"=>"water"}, :data=>[20, 30, 40, 50]}
# ],
# [{:type=>{"id"=>1, "name"=>"electricity"}, :data=>[15, 25, 35, 45]},
# {:type=>{"id"=>2, "name"=>"water"}, :data=>[25, 35, 45, 55]}
# ],
# [{:type=>{"id"=>1, "name"=>"electricity"}, :data=>[17, 27, 37, 47]},
# {:type=>{"id"=>2, "name"=>"water"}, :data=>[27, 37, 47, 57]}
# ]
# ]
If you are not certain that the elements of each array will be sorted by "id", you could write:
b = arr.map { |h| h[:report_data].sort_by { |g| g[:type]["id"] } }
c = b.transpose
#=> [
# [{:type=>{"id"=>1, "name"=>"electricity"}, :data=>[10, 20, 30, 40]},
# {:type=>{"id"=>1, "name"=>"electricity"}, :data=>[15, 25, 35, 45]},
# {:type=>{"id"=>1, "name"=>"electricity"}, :data=>[17, 27, 37, 47]}
# ],
# [{:type=>{"id"=>2, "name"=>"water"}, :data=>[20, 30, 40, 50]},
# {:type=>{"id"=>2, "name"=>"water"}, :data=>[25, 35, 45, 55]},
# {:type=>{"id"=>2, "name"=>"water"}, :data=>[27, 37, 47, 57]}
# ]
# ]
e = c.map {|d| { :type=>d.first[:type] }.
merge(:data=>d.map { |g| g[:data] }.transpose.map { |a| a.reduce(:+) }) }
#=> [{:type=>{"id"=>1, "name"=>"electricity"}, :data=>[42, 72, 102, 132]},
# {:type=>{"id"=>2, "name"=>"water"} , :data=>[72, 102, 132, 162]}]
Lastly, we need to put the put the key :report_data back in and add the months' data:
{ :months_data=>arr.first[:months_data], :report_data=>e }
#=> {:months_data=>{:months=>["jan", "feb"]},
# :report_data=>[
# {:type=>{"id"=>1, "name"=>"electricity"}, :data=>[42, 72, 102, 132]},
# {:type=>{"id"=>2, "name"=>"water"}, :data=>[72, 102, 132, 162]}
# ]
# }
For clarity I've reformatted the input array and removed the :monthes_data key, since that seems to be unrelated to your question. Here's our data:
TL;DR
def zip_sum(arr1, arr2)
return arr2 if arr1.nil?
arr1.zip(arr2).map {|a, b| a + b }
end
def sum_report_data(arr)
arr.flat_map do |item|
item[:report_data].map {|datum| datum.values_at(:type, :data) }
end
.reduce({}) do |sums, (type, data)|
sums.merge(type => data) do |_, old_data, new_data|
zip_sum(old_data, new_data)
end
end
.map {|type, data| { type: type, data: data } }
end
p sum_report_data(arr)
# =>
[ { type: { "id" => 1, "name" => "electricity" }, data: [ 42, 72, 102, 132 ] },
{ type: { "id" => 2, "name" => "water" }, data: [ 72, 102, 132, 162 ] }
]
Explanation
arr = [
{ report_data: [
{ type: { "id" => 1, "name" => "electricity" },
data: [ 10, 20, 30, 40 ]
},
{ type: { "id" => 2, "name" => "water" },
data: [ 20, 30, 40, 50 ]
}
]
},
{ report_data: [
{ type: { "id" => 1, "name" => "electricity" },
data: [ 15, 25, 35, 45 ]
},
{ type: { "id" => 2, "name" => "water" },
data: [ 25, 35, 45, 55 ]
}
]
},
{ report_data: [
{ type: { "id" => 1, "name" => "electricity" },
data: [ 17, 27, 37, 47 ]
},
{ type: { "id" => 2, "name" => "water" },
data: [ 27, 37, 47, 57 ]
}
]
}
]
Step 1
First, let's define a helper method to sum the values of two arrays:
def zip_sum(arr1, arr2)
return arr2 if arr1.nil?
arr1.zip(arr2).map {|a, b| a + b }
end
zip_sum([ 1, 2, 3 ], [ 10, 20, 30 ])
# => [ 11, 22, 33 ]
zip_sum(nil, [ 5, 6, 7 ])
# => [ 5, 6, 7 ]
The way zip_sum works is by "zipping" the two arrays together using Enumerable#zip (e.g. [1, 2].zip([10, 20]) returns [ [1, 10], [2, 20] ]), then adding each pair together.
Step 2
Next, let's use Enumerable#flat_map to get the parts of the data we care about:
result1 = arr.flat_map do |item|
item[:report_data].map {|datum| datum.values_at(:type, :data) }
end
# result1 =>
[ [ { "id" => 1, "name" => "electricity" }, [ 10, 20, 30, 40 ] ],
[ { "id" => 2, "name" => "water" }, [ 20, 30, 40, 50 ] ],
[ { "id" => 1, "name" => "electricity" }, [ 15, 25, 35, 45 ] ],
[ { "id" => 2, "name" => "water" }, [ 25, 35, 45, 55 ] ],
[ { "id" => 1, "name" => "electricity" }, [ 17, 27, 37, 47 ] ],
[ { "id" => 2, "name" => "water" }, [ 27, 37, 47, 57 ] ]
]
Above we've just grabbed the :type and :data values out of each hash the :report_data arrays.
Step 3
Next let's use Enumerable#reduce to iterate over the array of arrays and calculate a running sum of the :data values using the zip_sum method we defined earlier:
result2 = result1.reduce({}) do |sums, (type, data)|
sums.merge(type => data) do |_, old_data, new_data|
zip_sum(old_data, new_data)
end
end
# result2 =>
{ { "id" => 1, "name" => "electricity" } => [ 42, 72, 102, 132 ],
{ "id" => 2, "name" => "water" } => [ 72, 102, 132, 162 ]
}
The result might look a little odd to you because we usually use strings or symbols as hash keys, but in this hash we're using other hashes (the :type values from above) as keys. That's one nice thing about Ruby: You can use any object as a key in a hash.
Inside the reduce block, sums is the hash that's ultimately returned. It starts out as an empty hash ({}, the value we passed to reduce as an argument). type is the hash we're using as a key and data is the array of integers. In each iteration the next values from the result2 array are assigned to type, but sums is updated with whatever value was returned at the end of the block in the previous iteration.
We're using Hash#merge in kind of a tricky way:
sums.merge(type => data) do |_, old_data, new_data|
zip_sum(old_data, new_data)
end
This merges the hash { type => data } (remember that type is the :type hash
and data is the array of integers) into the hash sums. If there are any key collisions, the block will be invoked. Since we only have one key, type, then the block will be invoked if sums[type] already exists. If it does, we call zip_sum with the previous value of sums[type] and data, effectively keeping a running sum of data.
In effect, it's basically doing this:
sums = {}
type, data = result2[0]
sums[type] = zip_sum(sums[type], data)
type, data = result2[1]
sums[type] = zip_sum(sums[type], data)
type, data = result2[3]
# ...and so on.
Step 4
We now have this hash in result3:
{ { "id" => 1, "name" => "electricity" } => [ 42, 72, 102, 132 ],
{ "id" => 2, "name" => "water" } => [ 72, 102, 132, 162 ]
}
That's the data we want, so now we just have to take it out of this weird format and put it into a regular hash with the keys :type and :data:
result3 = result2.map {|type, data| { type: type, data: data } }
# result3 =>
[ { type: { "id" => 1, "name" => "electricity" },
data: [ 42, 72, 102, 132 ]
},
{ type: { "id" => 2, "name" => "water" },
data: [ 72, 102, 132, 162 ]
}
]