How to aggregate this data more efficiently? - ruby-on-rails

I am using Rail 3 with Mongoid as my ODM.
I have imported the following documents into MongoDB:
{ "make" : "Make A", "model": "Model 1", "variant" : "Variant 1" }
{ "make" : "Make B", "model": "Model 3", "variant" : "Variant 1" }
{ "make" : "Make A", "model": "Model 2", "variant" : "Variant 2" }
{ "make" : "Make A", "model": "Model 2", "variant" : "Variant 1" }
The following code produces a nested hash of sorted distinct values:
#makes = Item.all.distinct(:make).sort
#models = {}
#makes.each do |make|
#models[make] = Item.where(:make => make).distinct(:model).sort
end
#output = {}
#models.each_pair do |make, models|
#output[make] = {}
models.each do |model|
#output[make][model] = Item.where(:make => make, :model => model).distinct(:variant).sort
end
end
The resulting hash looks like this:
{
"Make A" => {
"Model 1" => ["Variant 1"],
"Model 2" => ["Variant 1", "Variant 2"]
},
"Make B" => {
"Model 3" => ["Variant 1"]
}
}
This all works fine, but is very inefficient as it involves so many queries. Is there a better way of achieving this, perhaps by having MongoDB perform the aggregation?

I solved this using MongoDB's MapReduce function with the following paramaters:
map = function() {
emit( 1, { make: this.make, model: this.model, variant: this.variant } );
}
reduce = function(key, values) {
var result = {};
values.forEach(function(value) {
if (!result[value.make]) result[value.make] = {};
if (!result[value.make][value.model]) result[value.make][value.model] = [];
result[value.make][value.model].push(value.variant);
});
return result;
}
This returns a single MongoDB result in the same format as the Ruby hash above.

Related

How to update the existing fields, embedded mongodb documents in an array in Java

I have document structure
{
"Name" : "MongoDB",
"Client" : "Database",
"Details" : [
{
"Date" : "2018-07-18",
"Code" : {
"new" : "5",
"new 1" : "3",
"new 2" : "4"
}
},
{
"Date" : "2018-07-19",
"Code" : {
"new" : "7",
"new 2" : "4"
}
}
]
}
I want to update the field "new":"5" as "Devops":"2, Based on "Name":"Mongodb","Date":"2018-07-18".
I tried this in Java,but this code appends the new document for the same date
Bson filter =new Document("Name",name);
Document h=new Document();
Document dd=new Document();
for(int i=0;i<ccode.length;i++)
{
h.append(ccode[i], hour[i]);
}
dd.append("Date", d);
dd.append("Code", h);
Document doc=new Document().append("Details",dd);
System.out.println(dd);
Bson updateOperationDocument = new Document("$push", doc);
collection.updateOne(filter, updateOperationDocument);

Iterate through a hash. However, my value is changing every time

I'm currently working on a simple hash loop, to manipulate some json data. Here's my Json data:
{
"polls": [
{ "id": 1, "question": "Pensez-vous utiliser le service de cordonnerie/pressing au moins 2 fois par mois ?" },
{ "id": 2, "question": "Avez-vous passé une bonne semaine ?" },
{ "id": 3, "question": "Le saviez-vous ? Il existe une journée d'accompagnement familial." }
],
"answers": [
{ "id": 1, "poll_id": 1, "value": true },
{ "id": 2, "poll_id": 3, "value": false },
{ "id": 3, "poll_id": 2, "value": 3 }
]
}
I want to have the poll_id value and the value from the answers hash. So here's what I code :
require 'json'
file = File.read('data.json')
datas = JSON.parse(file)
result = Hash.new
datas["answers"].each do |answer|
result["polls"] = {"id" => answer["poll_id"], "value" => answer["value"]}
end
polls_json = result.to_json
However, it returns me :
{
"polls": {
"id": 2,
"value": 3
}
}
Here's the output i am looking for :
{
"polls": [
{
"id": 1,
"value": true
},
{
"id": 2,
"value": 3
},
{
"id": 3,
"value": false
}
]
}
It seems that the value is not saved into my loop. I've tried different method but I still cannot find a solution .. Any suggestions?
You should be using reduce here, i.e.
datas["answers"].reduce({ polls: [] }) do |hash, data|
hash[:polls] << { id: data["poll_id"], value: data["value"] }
hash
end
This method iterates through the answers, making available the object supplied to reduce (in this case a hash with a :polls array) to which we pass each data hash.
I'd personally, um, reduce this a little further with the following, although it's at some cost to readability:
datas["answers"].reduce({ polls: [] }) do |hash, data|
hash.tap { |h| h[:polls] << { id: data["poll_id"], value: data["value"] } }
end
It's the cleanest method to achieve what you're looking for, using a built-for-purpose method.
Docs for reduce here: https://ruby-doc.org/core-2.1.0/Enumerable.html#method-i-reduce
(I'd also be inclined to update the variable names - data is already plural, so 'datas' is a little confusing to anyone else coming to your code.)
Edit: #max makes a great point re symbol / string keys from your data - keep that in mind if you attempt to apply this.
try the below:
require 'json'
file = File.read('data.json')
datas = JSON.parse(file)
result = Hash.new
poll_json = []
datas["answers"].each do |answer|
poll_json << {"id" => answer["poll_id"], "value" => answer["value"]}
end
p "json = "#{poll_json}"
{
polls: datas["answers"].map do |a|
{ id: a["poll_id"], value: a["value"] }
end
}
In general use .map to iterate through arrays and hashes and return new objects. .each should only be used when you are only concerned about the side effects (like in a view when you are outputting values).
require 'json'
json = JSON.parse(File.read('data.json'))
result = {
polls: json["answers"].map do |a|
{ id: a["poll_id"], value: a["value"] }
end
}
puts result.to_json
The output is:
{"polls":[{"id":1,"value":true},{"id":3,"value":false},{"id":2,"value":3}]}

How to merge arrays of hashes based on keys of hashes in Rails

I want to merge these two arrays based on uniqueness:
"template_variables": [{
"info_top": "Some string"
}, {
"info_bottom": "Other string"
}],
"user_variables": [{
"info_top": "Default string"
}, {
"info_bottom": "Other default string"
}, {
"other_info": "Default number"
}]
So if I start with the user_variables array and add template_variables to it, replacing hashes where matches are found.
My desired output would be:
"new_variables": [{
"info_top": "Some string"
}, {
"info_bottom": "Other string"
}, {
"other_info": "Default number"
}]
I've tried user_variables.merge(template_variables) and variations on that, but that's not suitable for an array of hashes, it seems.
How do I do this?
(first_array + second_array).uniq{|hash| hash.keys.first}
but your data structure sucks.
If:
hash = { "template_variables":
[{"info_top": "Some string"},
{"info_bottom": "Other string"}],
"user_variables":
[{"info_top": "Default string"},
{"info_bottom": "Other default string"},
{"other_info": "Default number"}]
}
Then try this:
values = hash.values.flatten.reverse.inject(&:merge!).map { |k,v| { k => v } }
new_hash = {"new_variables": values}
returns:
{ :new_variables =>
[{:other_info => "Default number"},
{:info_bottom => "Other string"},
{:info_top => "Some string"}]
}
You can simply do something like
kk = {"aa": [{a: 1}, {b: 2}]}
jk = {"bb": [{a:3}, {d: 4}]}
(kk.values+jk.values).flatten.uniq{|hash| hash.keys.first}
Which is similar to Mladen Jablanović post

Ruby refactoring: converting array to hash

Here's what I get in Rails params:
obj => {
"raw_data" =>
[
{ "id" => "1", "name" => "John Doe" },
{ "id" => "2", "name" => "Jane Doe" }
]
}
I have to transform into a following object:
obj => {
"data" =>
{
"1" => { "name" => "John Doe" },
"2" => { "name" => "Jane Doe" }
}
}
Here's the code I have working so far:
if obj[:raw_data]
obj[:data] = Hash.new
obj[:raw_data].each do |raw|
obj[:data][raw[:id]] = Hash.new
obj[:data][raw[:id]][:name] = raw[:name] if raw[:name].present?
end
end
obj.delete(:raw_data)
Is there a way to refactor it? Maybe using map. Note that data structure has to change from array to hash as well.
Thanks for any tips.
Here's one way:
obj = {
"raw_data" => [
{ "id" => "1", "name" => "John Doe" },
{ "id" => "2", "name" => "Jane Doe" }
]
}
data = obj["raw_data"].map do |item|
item = item.dup
[ item.delete('id'), item ]
end
obj2 = { "data" => data.to_h }
# => { "data" =>
# { "1" => { "name" => "John Doe" },
# "2" => { "name" => "Jane Doe" }
# }
# }
If you're using Rails you can use the Hash#except method from ActiveSupport to make it a little more succinct:
data = obj["raw_data"].map {|item| [ item["id"], item.except("id") ] }
obj2 = { "data" => data.to_h }
d = obj[:raw_data]
keys = d.map { |h| h["id"] }
values = d.map { |h| h.except("id") }
Hash[ keys.zip(values) ]
# or as a oneliner
Hash[ d.map { |h| h["id"] }.zip(d.map { |h| h.except("id")) ]
# => {"1"=>{"name"=>"John Doe"}, "2"=>{"name"=>"Jane Doe"}}
This special Hash[] syntax lets you create a hash from a array of keys and an array of values.
Hash.except(*args) is an ActiveSupport addition to the hash class which returns a new key without the keys in the blacklist.
In rails, you can use index_by method:
obj = {raw_data: [{id: "1", name: "John Doe"}, {id: "2", name: "Jane Doe"}]}
obj2 = {
data: obj[:raw_data].index_by {|h| h[:id]}.each {|_,h| h.delete(:id)}
} #=> {:data=>{"1"=>{:name=>"John Doe"}, "2"=>{:name=>"Jane Doe"}}}
One downfall of this is that it will modify the original data by deleting id property. If this is unacceptable, here is modified, safe version:
obj2 = {
data: obj[:raw_data].map(&:clone).index_by {|h| h[:id]}.each {|_,h| h.delete(:id)}
} #=> {:data=>{"1"=>{:name=>"John Doe"}, "2"=>{:name=>"Jane Doe"}}}
I assume you mean obj = {...} and not obj => {...}, as the latter is not a valid object. If so:
{ "data" => obj["raw_data"].each_with_object({}) { |g,h|
h[g["id"]] = g.reject { |k,_| k == "id" } } }
#=> {"data"=>{"1"=>{"name"=>"John Doe"}, "2"=>{"name"=>"Jane Doe"}}}
If obj can be mutated, you can simplify a bit:
{ "data" => obj["raw_data"].each_with_object({}) { |g,h| h[g.delete("id")]=g } }
As an improved non-mutating solution, #Max suggested a Rails' tweak:
{ "data" => obj["raw_data"].each_with_object({}) { |g,h| h[g["id"]] = g.except("id") } }
That looks good to me, but as I don't know rails, I'm taking that advice at face value.

Rails Model Syntax Confusion

I came across this code in Rails app using mongodb:
"""
Folder format:
{
name: <folder name>,
stocks: [
{
name: <stock name>,
id: <stock id>,
qty: <stock quantity>
}
]
}
"""
def format_with_folders(stocks)
fmap = stock_folder_map
res = stocks.group_by {|s| fmap[s["id"]] }.collect {|fname, ss|
{
"name" => fname,
"stocks" => ss
}
}
new(folders: res)
end
def stock_folder_map
res = {}
folders.each { |ff|
ff.stocks.each { |s|
res[s["id"]] = ff["name"]
}
}
return res
end
end
The doubts are:
1) What does the code inside triple quote signify? Is is a commented code?
2)What would be the right format to use this code inside a ruby script?
First of all, the triple quoted string is often used as a comment, and that is the case here.
To get this to work outside of the class, you would need create a folders method that returns an array of folders in the correct structure. You could do something like this:
Folder = Struct.new(:name, :stocks)
def folders
[
Folder.new(
"Folder 1",
[
{ "name" => "stock name", "id" => "stock id", "qty" => 3 },
{ "name" => "stock name", "id" => "stock id", "qty" => 5 }
]
),
Folder.new(
"Folder 2",
[
{ "name" => "stock name", "id" => "stock id", "qty" => 2 },
{ "name" => "stock name", "id" => "stock id", "qty" => 1 }
]
)
]
end
def format_with_folders(stocks)
# ...
end
def stock_folder_map
# ...
end
The folders method returns an array of Folder objects, which both have a name and stocks attribute. Stocks are an array of hashes.
In Ruby, if you have multiple string literals next to each other, they get concatenated at parse time:
'foo' "bar"
# => 'foobar'
This is a feature inspired by C.
So, what you have there is three string literals next to each other. The first string literal is the empty string:
""
Then comes another string literal:
"
Folder format:
{
name: <folder name>,
stocks: [
{
name: <stock name>,
id: <stock id>,
qty: <stock quantity>
}
]
}
"
And lastly, there is a third string literal which is again empty:
""
At parse time, this will be concatenated into a single string literal:
"
Folder format:
{
name: <folder name>,
stocks: [
{
name: <stock name>,
id: <stock id>,
qty: <stock quantity>
}
]
}
"
And since this string object isn't referenced by anything, isn't assigned to any variable, isn't returned from any method or block, it will just get immediately garbage collected.
In other words: the entire thing is a no-op, it's dead code. A sufficiently smart Ruby compiler (such as JRuby or Rubinius) will probably completely eliminate it, compile it into nothing.

Resources