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.
Related
I'm just planning to switch from Orders collection structure to Items structure with a query.
Of course I'm able to to make it by iterating and with a few lines coding, but I'm sure there is an easier way to achieve it with a mongo query.
Each document in Orders collection can contain a User and many itemIds.
From this collection I would like to get an Items collection output with a query. What sort of aggregation/projection I need to use to get UserIds linked to each item ?
Source, Orders collection
{
"UserId" : "Acme",
"ItemIds" : [
1,
2,
3
]
},.....
Destination, Items Collection
{
"ItemId" : 1,
"UserIds" : [
1,
3
]
},....
P.S. this is not a DB design question and the number of items in Orders and itemIds are finite. I made up them to explain the problem.
You could use an aggregation pipeline with 3 stages:
$unwind itemIDs
$group by itemID, $push UserId into an array
$out to new collection
Similar to Joe's method, I used this to 'invert' a parent-child relationship:
the Orders data looks like:
[
{ userId: 1, itemIds: [5, 6, 7]},
{ userId: 2, itemIds: [7, 8, 9]},
{ userId: 1, itemIds: [4, 5, 6]},
{ userId: 3, itemIds: [5, 6, 7]}
]
Mongo Query:
db.getCollection('Orders').aggregate([
{$unwind: '$itemIds'},
{$project: {userId: 1, itemIds: 1}},
{$sort: {itemIds: 1, userId: 1}},
{$group: { _id: "$itemIds", userIds: { $push : "$userId"} }},
{$sort: { _id: 1 }}
])
$sort is optional, of course
the result should look like:
[
{ itemId: 4: userIds: [1] },
{ itemId: 5: userIds: [1, 1, 3] },
{ itemId: 6: userIds: [1, 1, 3] },
{ itemId: 7: userIds: [1, 2, 3] },
{ itemId: 8: userIds: [2] },
{ itemId: 9: userIds: [2] },
]
If you want unique userIds
{$group: { _id: "$itemIds", userIds: { $addToSet : "$userId"} }},
I would like to plot a graph in AngularJS, using Highcharts, to be something like this:
This graph represents the load of a server in the last hour. So the datapoints given, contains a point and epoch time.
The data is received in a JSON format, [point,epochtime], as follows:
[
{
"ds_name": "a0",
"cluster_name": "",
"graph_type": "stack",
"host_name": "",
"metric_name": "1-min",
"color": "#BBBBBB",
"datapoints": [
[
0.58,
1604244900
],
[
0.59733333333,
1604244915
],
[
0.6,
1604244930
],
[
0.6,
1604244945
],
[
0.6,
1604244960
],
[
0.6,
1604244975
],
[
0.612,
1604244990
]
]
},
{
"ds_name": "a2",
"cluster_name": "",
"graph_type": "line",
"host_name": "",
"metric_name": "CPUs ",
"color": "#FF0000",
"datapoints": [
[
2,
1604244900
],
[
2,
1604244915
],
[
2,
1604244930
],
[
2,
1604244945
],
[
2,
1604244960
],
[
2,
1604244975
],
[
2,
1604244990
],
[
2,
1604245005
]
]
},
{
"ds_name": "a3",
"cluster_name": "",
"graph_type": "line",
"host_name": "",
"metric_name": "Procs",
"color": "#2030F4",
"datapoints": [
[
1,
1604244900
],
[
1,
1604244915
],
[
1,
1604244930
]
]
}
]
I posted here only part of the dataset, since it is too long, but I think you can understand the format.
How can I draw something similar using Highcharts?
The most important thing here is to properly parse the date. Since the names in the JSON are not the same as Highcharts requires you have to pass them manually to the right places as I did it in the demo below.
I also recommend using the keys property because the data should be declared as [x, y], in your case it is the other way around. You can change that by setting this property.
Notice that in the Highcharts there is no such series as stack, it is just column.
API: https://api.highcharts.com/highcharts/series.line.keys
Demo: https://jsfiddle.net/BlackLabel/246phxum/
So this is how I did it eventually, the most suitable types of charts are line, and area chart.
Highcharts.chart("container", {
chart: {
type: 'line'
},
xAxis: {
type: "datetime",
labels: {
formatter: function () {
return Highcharts.dateFormat('%H:%M:%S.%L', this.value);
}
}
},
series: [{
name: "A",
type: "area",
color: "#BB000B",
keys: ["y", "x"],
data: [
[0.58, 1604244900],
[0.59733333333, 1604244915],
[0.6, 1604244930],
[0.6, 1604244945],
[0.6, 1604244960],
[0.6, 1604244975],
[0.612, 1604244990]
]
},
{
name: "B",
type: "line",
color: "#FF00F0",
keys: ["y", "x"],
data: [
[2, 1604244900],
[2, 1604244915],
[2, 1604244930],
[2, 1604244945],
[2, 1604244960],
[2, 1604244975],
[2, 1604244990],
[2, 1604245005]
]
}, {
name: 'C',
keys: ['y', 'x'],
data: [
[1, 1604244900],
[1, 1604244915],
[1, 1604244930],
[1, 1604244945],
[1, 1604244960],
[1, 1604244975],
[1, 1604244990],
[1, 1604245005]
]
}
]
});
I'm trying to place these tools into the right group hash object in this array. I'm not sure how to do this with Ruby.
groups = [
{ group: 'Business Training', tools: [] },
{ group: 'Human Resources', tools: [] },
{ group: 'Clean', tools: [] },
{ group: 'Example', tools: [] }
]
tools = [
{ name: "Foo", group: "Clean", id: 1 },
{ name: "Bar", group: "Clean", id: 2 },
{ name: "Baz", group: "Business Training", id: 3 },
]
I want to end up with a structure like this:
groups = [
{
group: 'Business Training',
tools: [
{ name: "Baz", group: "Business Training", id: 3 },
]
},
{ group: 'Human Resources', tools: [] },
{
group: 'Clean',
tools: [
{ name: "Foo", group: "Clean", id: 1 },
{ name: "Bar", group: "Clean", id: 2 },
]
},
{ group: 'Example', tools: [] }
]
tools.each do |tool|
group = groups.find { |item| item[:group] == tool[:group] }
group[:tools] << tool
end
result = groups.map do |group|
{
group: group[:group],
tools: tools.select do |tool|
tool[:group] == group[:group]
end
}
end
puts result
Which prints:
{:group=>"Business Training", :tools=>[{:name=>"Baz", :group=>"Business Training", :id=>3}]}
{:group=>"Human Resources", :tools=>[]}
{:group=>"Clean", :tools=>[{:name=>"Foo", :group=>"Clean", :id=>1}, {:name=>"Bar", :group=>"Clean", :id=>2}]}
{:group=>"Example", :tools=>[]}
One more solution:
groups.each do |group|
group[:tools] =
tools.select { |tool| tool[:group] == group[:group] }
end
h = tools.each_with_object({}) { |g,h| h[g[:group]] = g }
#=> { "Clean" =>{:name=>"Bar", :group=>"Clean", :id=>2},
# "Business Training"=>{:name=>"Baz", :group=>"Business Training", :id=>3}}
groups.map { |g| g.update(tools: h[g[:group]]) }
#=> [{:group=>"Business Training",
# :tools=>{:name=>"Baz", :group=>"Business Training", :id=>3}},
# {:group=>"Human Resources",
# :tools=>nil},
# {:group=>"Clean",
# :tools=>{:name=>"Bar", :group=>"Clean", :id=>2}},
# {:group=>"Example",
# :tools=>nil}]
Constructing the hash h requires only a single pass through tools. For each element of groups this permits a simple hash value replacement, which is faster than methods that search though tools for each element of groups or vice-versa.
Notice that, as well as mutating groups, this returns the updated value. If you do not wish to modify groups replace Hash#update (aka merge!) with Hash#merge.
I have an array of objects like this:
[
{
"id": 2,
"title": "LA COPA",
"parent_menu_id": nil
},
{
"id": 3,
"title": "CALENDARIO",
"parent_menu_id": nil
},
{
"id": 5,
"title": "Torneo",
"parent_menu_id": 2
},
{
"id": 6,
"title": "Nice",
"parent_menu_id": 2
}
]
This is the structure of a menu.
Each object is an menu item.
If the key "parent_menu_id" is nil, it means that the item is a parent menu.
If it has a value, is a children. I.E the third item means that item id: 5 is child of item id:2.
This is the desired output:
[
{
"id": 2,
"title": "LA COPA",
"active": true,
"parent_menu_id": nil,
"submenus":[
{
"id": 5,
"title": "Torneo",
"active": true,
"parent_menu_id": 2
},
{
"id": 6,
"title": "Nice",
"parent_menu_id": 2
}
]
},
{
"id": 3,
"title": "CALENDARIO",
"active": true,
"parent_menu_id": nil
}
]
I know the algorithm:
if pareny_menu_id is different than nill search the key id == parent_menu_id
If submenu key is not present, create it.
Move the child item to the parent.
Then delete the child item from base location.
But I am not sure about the methods to use..
Any ideas?
Cheers!
#!ruby
require 'pp'
objects = [
{
"id" => 2,
"title" => "LA COPA",
"parent_menu_id" => nil
},
{
"id" => 3,
"title" => "CALENDARIO",
"parent_menu_id" => nil
},
{
"id" => 5,
"title" => "Torneo",
"parent_menu_id" => 2
},
{
"id" => 6,
"title" => "Nice",
"parent_menu_id" => 2
}
]
objects.collect! do |child|
if not child["parent_menu_id"].nil? then
parents = objects.select{|o| o["id"] == child["parent_menu_id"]}
parents.each do |pr|
pr["submenus"] ||= []
pr["submenus"] << child
end
end
child
end.select!{|o| o["parent_menu_id"].nil? }
pp objects
output:
[{"id"=>2,
"title"=>"LA COPA",
"parent_menu_id"=>nil,
"submenus"=>
[{"id"=>5, "title"=>"Torneo", "parent_menu_id"=>2},
{"id"=>6, "title"=>"Nice", "parent_menu_id"=>2}]},
{"id"=>3, "title"=>"CALENDARIO", "parent_menu_id"=>nil}]
arr = [
{
id: 2,
title: "LA COPA",
parent_menu_id: nil
},
{
id: 3,
title: "CALENDARIO",
parent_menu_id: nil
},
{
id: 5,
title: "Torneo",
parent_menu_id: 2
},
{
id: 6,
title: "Nice",
parent_menu_id: 2
}
]
parents, children = arr.partition { |g| g[:parent_menu_id].nil? }
#= [[{:id=>2, :title=>"LA COPA", :parent_menu_id=>nil},
# {:id=>3, :title=>"CALENDARIO", :parent_menu_id=>nil}],
# [{:id=>5, :title=>"Torneo", :parent_menu_id=>2},
# {:id=>6, :title=>"Nice", :parent_menu_id=>2}]]
children.each do |child|
parent_id = child[:parent_menu_id]
parent = parents.find { |p| p[:id] == parent_id }
if parent.key?(:submenus)
parent[:submenus] << child
else
parent[:submenus] = [child.merge(active: true)]
end
end
parents
#=> [{:id=>2, :title=>"LA COPA", :parent_menu_id=>nil,
# :submenus=>[{:id=>5, :title=>"Torneo", :parent_menu_id=>2, :active=>true},
# {:id=>6, :title=>"Nice", :parent_menu_id=>2}]},
# {:id=>3, :title=>"CALENDARIO", :parent_menu_id=>nil}]
Notes:
"id": 2 is redundant. Just use id: 2. You only need quotes when the symbol is comprised of more than one word.
I assumed that the key value pair active: true is only present for the first element of the value (array) of :submenus.
I am querying for finding exact array match and retrieved it successfully but when I try to find out the exact array with values in different order then it get fails.
Example
db.coll.insert({"user":"harsh","hobbies":["1","2","3"]})
db.coll.insert({"user":"kaushik","hobbies":["1","2"]})
db.coll.find({"hobbies":["1","2"]})
2nd Document Retrieved Successfully
db.coll.find({"hobbies":["2","1"]})
Showing Nothing
Please help
The currently accepted answer does NOT ensure an exact match on your array, just that the size is identical and that the array shares at least one item with the query array.
For example, the query
db.coll.find({ "hobbies": { "$size" : 2, "$in": [ "2", "1", "5", "hamburger" ] } });
would still return the user kaushik in that case.
What you need to do for an exact match is to combine $size with $all, like so:
db.coll.find({ "hobbies": { "$size" : 2, "$all": [ "2", "1" ] } });
But be aware that this can be a very expensive operation, depending on your amount and structure of data.
Since MongoDB keeps the order of inserted arrays stable, you might fare better with ensuring arrays to be in a sorted order when inserting to the DB, so that you may rely on a static order when querying.
To match the array field exactly Mongo provides $eq operator which can be operated over an array also like a value.
db.collection.find({ "hobbies": {$eq: [ "singing", "Music" ] }});
Also $eq checks the order in which you specify the elements.
If you use below query:
db.coll.find({ "hobbies": { "$size" : 2, "$all": [ "2", "1" ] } });
Then the exact match will not be returned. Suppose you query:
db.coll.find({ "hobbies": { "$size" : 2, "$all": [ "2", "2" ] } });
This query will return all documents having an element 2 and has size 2 (e.g. it will also return the document having hobies :[2,1]).
Mongodb filter by exactly array elements without regard to order or specified order.
Source: https://savecode.net/code/javascript/mongodb+filter+by+exactly+array+elements+without+regard+to+order+or+specified+order
// Insert data
db.inventory.insertMany([
{ item: "journal", qty: 25, tags: ["blank", "red"], dim_cm: [ 14, 21 ] },
{ item: "notebook", qty: 50, tags: ["red", "blank"], dim_cm: [ 14, 21 ] },
{ item: "paper", qty: 100, tags: ["red", "blank", "plain"], dim_cm: [ 14, 21 ] },
{ item: "planner", qty: 75, tags: ["blank", "red"], dim_cm: [ 22.85, 30 ] },
{ item: "postcard", qty: 45, tags: ["blue"], dim_cm: [ 10, 15.25 ] }
]);
// Query 1: filter by exactly array elements without regard to order
db.inventory.find({ "tags": { "$size" : 2, "$all": [ "red", "blank" ] } });
// result:
[
{
_id: ObjectId("6179333c97a0f2eeb98a6e02"),
item: 'journal',
qty: 25,
tags: [ 'blank', 'red' ],
dim_cm: [ 14, 21 ]
},
{
_id: ObjectId("6179333c97a0f2eeb98a6e03"),
item: 'notebook',
qty: 50,
tags: [ 'red', 'blank' ],
dim_cm: [ 14, 21 ]
},
{
_id: ObjectId("6179333c97a0f2eeb98a6e05"),
item: 'planner',
qty: 75,
tags: [ 'blank', 'red' ],
dim_cm: [ 22.85, 30 ]
}
]
// Query 2: filter by exactly array elements in the specified order
db.inventory.find( { tags: ["blank", "red"] } )
// result:
[
{
_id: ObjectId("6179333c97a0f2eeb98a6e02"),
item: 'journal',
qty: 25,
tags: [ 'blank', 'red' ],
dim_cm: [ 14, 21 ]
},
{
_id: ObjectId("6179333c97a0f2eeb98a6e05"),
item: 'planner',
qty: 75,
tags: [ 'blank', 'red' ],
dim_cm: [ 22.85, 30 ]
}
]
// Query 3: filter by an array that contains both the elements without regard to order or other elements in the array
db.inventory.find( { tags: { $all: ["red", "blank"] } } )
// result:
[
{
_id: ObjectId("6179333c97a0f2eeb98a6e02"),
item: 'journal',
qty: 25,
tags: [ 'blank', 'red' ],
dim_cm: [ 14, 21 ]
},
{
_id: ObjectId("6179333c97a0f2eeb98a6e03"),
item: 'notebook',
qty: 50,
tags: [ 'red', 'blank' ],
dim_cm: [ 14, 21 ]
},
{
_id: ObjectId("6179333c97a0f2eeb98a6e05"),
item: 'planner',
qty: 75,
tags: [ 'blank', 'red' ],
dim_cm: [ 22.85, 30 ]
}
]
This query will find exact array with any order.
let query = {$or: [
{hobbies:{$eq:["1","2"]}},
{hobbies:{$eq:["2","1"]}}
]};
db.coll.find(query)
with $all we can achieve this.
Query : {cast:{$all:["James J. Corbett","George Bickel"]}}
Output : cast : ["George Bickel","Emma Carus","George M. Cohan","James J. Corbett"]
Using aggregate this is how I got mine proficient and faster:
db.collection.aggregate([
{$unwind: "$array"},
{
$match: {
"array.field" : "value"
}
},
You can then unwind it again for making it flat array and then do grouping on it.
This question is rather old, but I was pinged because another answer shows that the accepted answer isn't sufficient for arrays containing duplicate values, so let's fix that.
Since we have a fundamental underlying limitation with what queries are capable of doing, we need to avoid these hacky, error-prone array intersections. The best way to check if two arrays contain an identical set of values without performing an explicit count of each value is to sort both of the arrays we want to compare and then compare the sorted versions of those arrays. Since MongoDB does not support an array sort to the best of my knowledge, we will need to rely on aggregation to emulate the behavior we want:
// Note: make sure the target_hobbies array is sorted!
var target_hobbies = [1, 2];
db.coll.aggregate([
{ // Limits the initial pipeline size to only possible candidates.
$match: {
hobbies: {
$size: target_hobbies.length,
$all: target_hobbies
}
}
},
{ // Split the hobbies array into individual array elements.
$unwind: "$hobbies"
},
{ // Sort the elements into ascending order (do 'hobbies: -1' for descending).
$sort: {
_id: 1,
hobbies: 1
}
},
{ // Insert all of the elements back into their respective arrays.
$group: {
_id: "$_id",
__MY_ROOT: { $first: "$$ROOT" }, // Aids in preserving the other fields.
hobbies: {
$push: "$hobbies"
}
}
},
{ // Replaces the root document in the pipeline with the original stored in __MY_ROOT, with the sorted hobbies array applied on top of it.
// Not strictly necessary, but helpful to have available if desired and much easier than a bunch of 'fieldName: {$first: "$fieldName"}' entries in our $group operation.
$replaceRoot: {
newRoot: {
$mergeObjects: [
"$__MY_ROOT",
{
hobbies: "$hobbies"
}
]
}
}
}
{ // Now that the pipeline contains documents with hobbies arrays in ascending sort order, we can simply perform an exact match using the sorted target_hobbies.
$match: {
hobbies: target_hobbies
}
}
]);
I cannot speak for the performance of this query, and it may very well cause the pipeline to become too large if there are too many initial candidate documents. If you're working with large data sets, then once again, do as the currently accepted answer states and insert array elements in sorted order. By doing so you can perform static array matches, which will be far more efficient since they can be properly indexed and will not be limited by the pipeline size limitation of the aggregation framework. But for a stopgap, this should ensure a greater level of accuracy.