Ruby on Rails - Gather hashes with same specific value - ruby-on-rails

Given a an array of hashes where every hash is like {"date":"date_value", "slots":[slots_value]}, I'd like to gather hashes with the same dates on one hash and merge slots arrays.
Example input:
[{"date" : "2016/23/12", "slots" : ["a","b"]},
{"date" : "2016/23/12", "slots" : ["c","d","e"]},
{"date" : "2016/24/12", "slots" : ["x"]}
]
Example output:
[{"date" : "2016/23/12", "slots" : ["a","b","c","d","e"]},
{"date" : "2016/24/12", "slots" : ["x"]}
]

Enumerable#group_by is a very powerful tool for Hashes and Arrays :
input = [
{"date" => "2016/23/12" , "slots" => ["a","b"]},
{"date" => "2016/23/12", "slots" => ["c","d","e"]},
{"date" => "2016/24/12", "slots" => ["x"]}
]
puts input.group_by{|h| h["date"]}.map{|date, hashes|
{
"date" => date,
"slots" => hashes.map{|h| h["slots"]}.flatten
}
}
#=> {"date"=>"2016/23/12", "slots"=>["a", "b", "c", "d", "e"]}
# {"date"=>"2016/24/12", "slots"=>["x"]}

Related

how to iterate through json result in ruby on rails?

I have a function in ruby on rails, where I make a call and get the following json data. now i want to add another “key” : "value" to each item in the json data and return the new or modified json object from the function. what is the best way to do this in ruby?
def get_results
results = getJsonData();
end
[
{ "name" : "Harry Potter", "rating" : 1, },
{ "name" : "Lord of the rings", "rating" : 2, },
{ "name" : "game of thrones", "rating" : 3, },
]
You can use map to transform every element of a collection
def get_results
results = getJsonData();
results.map { |item| item.merge("year" => 2000) }
end
In this case I'm adding a key "year" to your hashes with value 2000, it depends what you want to do

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.

Updating an array field that contains hashes in mongoid

I have a document in XYZ collection as follows.
"_id" : ObjectId("55311e4487216d7063040000"),
"colours" : [
{
"value" : 1,
"colour" : "red"
},
{
"value" : 2,
"colour" : "green"
}
]
I need to update the name of the colour which value is 1. What query should I write?
I am using rails 4.1.2, mongoid 4.0.0.
Please help.
For example you want to change it to "yellow" :
XYZ.where(_id: "55311e4487216d7063040000").elem_match(colours: { value: 1 }).update("$set" => {"colours.$.colour" => "yellow"})

Ruby on Rails: Concatenate results of Mongoid criterias and paging

I'm pretty sure that I'm doing something wrong. Consider the following code:
criteria1 = Model.where(...)
criteria2 = Model.where(...)
results = (criteria1.to_a + criteria2.to_a)[offset..(offset + count_per_page - 1)]
This code concatenates results of two different criterias and get a certain number of results with a given offset (paging).
The problem in this code is implicit. The to_a method call actually loads all results of a criteria to the memory as an array.
Now consider a really huge collection... The to_a call slows all things down dramatically.
What I wish to do is something like this:
criteria1 = Model.where(...)
criteria2 = Model.where(...)
# A criteria, which returns results of the first criteria concatenated with results of the second criteria
criteria = criteria1 + criteria2
results = criteria.offset(offset).limit(count_per_page)
The important thing is that results of the second criteria goes after results of the first criteria.
Any clues how is it possible to achieve with Mongoid?
Thanks!
UPDATE
Gergo Erdosi suggested to use merge method. I've tried to use this and it is not what I'm looking for. The problem here is the following:
criteria1 = Model.where(:name => "John", :age => "23")
criteria2 = Model.where(:name => "Bob", :gender => "male")
criteria = criteria1.merge(criteria2)
p criteria.selector
# prints: { "name" => "Bob", :age => 23, :gender => "male" }
So here are two problems:
merge doesn't produce OR, it overrides common keys of the first query with the second;
Even if we use Model.or({ :name => "John" }, { :name => "Bob" }) or Model.in(:name => ["John", "Bob"]) results won't have the right order. I wish results of the first criteria go first and then results of the second criteria go after.
It is possible that I don't understand something and Gergo's answer is right. Do you have any other ideas? Thanks.
UPDATE 2
Thank you Gergo for helping me out here. Let's try a simple example in Mongo shell:
// Fill out test db with some simple documents.
for (var i = 0; i < 10; ++i) { db.users.insert({ name: i % 2 ? "John" : "Bob", age: Math.round(Math.random() * 100) }); }
// These queries give me the same order of documents.
db.users.find({ name: { $in: ["Bob", "John"] } });
db.users.find({ $or: [{ name: "Bob" }, { name: "John" }] });
// Like this:
{ "_id" : ObjectId("53732076b110ab9be7619a8e"), "name" : "Bob", "age" : 69 }
{ "_id" : ObjectId("53732076b110ab9be7619a8f"), "name" : "John", "age" : 63 }
{ "_id" : ObjectId("53732076b110ab9be7619a90"), "name" : "Bob", "age" : 25 }
{ "_id" : ObjectId("53732076b110ab9be7619a91"), "name" : "John", "age" : 72 }
// ...
// But I wish to get concatenated results of these queries:
db.users.find({ name: "Bob" });
db.users.find({ name: "John" });
// Like this (results of the first criteria go first):
{ "_id" : ObjectId("53732076b110ab9be7619a8e"), "name" : "Bob", "age" : 69 }
{ "_id" : ObjectId("53732076b110ab9be7619a90"), "name" : "Bob", "age" : 25 }
// ...
{ "_id" : ObjectId("53732076b110ab9be7619a8f"), "name" : "John", "age" : 63 }
{ "_id" : ObjectId("53732076b110ab9be7619a91"), "name" : "John", "age" : 72 }
// ...
Notice, that I cannot use a simple sorting here, because the data in the real application is more complex. In the real application criterias look like these:
// query variable is a string
exact_match_results = Model.where(:name => query)
inexact_match_results = Model.where(:name => /#{query}/i)
So we cannot just sort alphabetically here.
Use the merge method:
criteria = criteria1.merge(criteria2)
results = criteria.offset(offset).limit(count_per_page)
You can see the details in the method description.
Edit: As pointed out, merge doesn't produce an OR query.
irb(main):010:0> Model.where(name: 'John').merge(Model.where(name: 'Bob'))
=> #<Mongoid::Criteria
selector: {"name"=>"Bob"}
options: {}
class: Model
embedded: false>
Which is not the expected behavior in this case. The reason is that merge uses Hash.merge which behaves this way. The relevant code from Criteria.merge:
selector.merge!(criteria.selector)
This can be illustrated as:
irb(main):011:0> {name: 'John'}.merge({name: 'Bob'})
=> {:name=>"Bob"}
Because of this, it's not easy to give a general advice on how to merge two criteria in a way that the result is an OR query. But with a little change in the criteria, it's possible. For example:
criteria1 = Model.any_of(name: 'John').where(age: '23')
criteria2 = Model.any_of(name: 'Bob').where(gender: 'male')
The result of the merge is an OR query which contains both names:
irb(main):014:0> criteria1.merge(criteria2)
=> #<Mongoid::Criteria
selector: {"$or"=>[{"name"=>"John"}, {"name"=>"Bob"}], "age"=>"23", "gender"=>"male"}
options: {}
class: Model
embedded: false>

Tire gem: How to access Elasticsearch's 'highlight' property?

I have some Rails models that are indexed in Elasticsearch (via Tire gem). I can index new documents and query the existing index.
What I can't seem to do is get ahold of the highlight attached to a record from within my Rails app. I can however see that highlight is returned in the json when I interact with Elasticsearch directly via curl.
When I try to access the highlight property of my record I get: undefined method 'highlight' for #<Report:0x007fe8afa54700>
# app/views/reports/index.html.haml
%h1 Listing reports
...
- #reports.results.each do |report|
%tr
%td= report.title
%td= raw report.highlight.attachment.first.to_s
But if I use curl I can see the highlight is returned to Tire...
$ curl -X GET "http://localhost:9200/testapp_development_reports/report/_search?load=true&pretty=true" -d '{query":{"query_string":{"query":"contains","default_operator":"AND"}},"highlight":{"fields":{"attachment":{}}}}'
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 0.111475274,
"hits" : [ {
"_index" : "testapp_development_reports",
"_type" : "report",
"_id" : "1",
"_score" : 0.111475274, "_source" : {"id":1,"title":"Sample Number One",...,"attachment":"JVBERi0xMJ1Ci... ...UlRU9GCg==\n"},
"highlight" : {
"attachment" : [ "\nThis <em>contains</em> one\n\nodd\n\n\n" ]
}
}, {
"_index" : "testapp_development_reports",
"_type" : "report",
"_id" : "2",
"_score" : 0.111475274, "_source" : {"id":2,"title":"Number two",...,"attachment":"JVBERi0xLKM3OA... ...olJVPRgo=\n"},
"highlight" : {
"attachment" : [ "\nThis <em>contains</em> two\n\neven\n\n\n" ]
}
} ]
}
}
The search method in the model:
...
def self.search(params)
tire.search(load: true) do
query { string params[:query], default_operator: "AND" } if params[:query].present?
highlight :attachment
end
end
...
Method highlight is inaccessible when you are using load: true option. This should be fixed in future versions of Tire.
edit: you can use each_with_hit method to access returned elasticsearch values now
For example:
results = Article.search 'One', :load => true
results.each_with_hit do |result, hit|
puts "#{result.title} (score: #{hit['_score']})"
end
You can find my answer right at this post
Elasticsearch/Lucene highlight
My method works fine for me and wish you can get it work as well.

Resources