How to convert hash with keys representing nesting into a nested hash - ruby-on-rails

I need to convert the following hash:
{
"item[0][size]" => "12",
"item[0][count]" => "1"
}
to this:
{
"item": {
"0": {
"size": "12",
"count": "1"
}
}
}
Could you please advice on how to achieve that most gracefully? Maybe I can reuse some ActionPack's utility method that is used for parsing parameter strings?

You can reuse a rack lib method Rack::Utils.parse_nested_query
require "rack"
def p p
Rack::Utils.parse_nested_query(p)
end
p 'item[0][size]=12' # => {"item"=>{"0"=>{"size"=>"12"}}}
Found here.

After some research I found a way to parse nested query keys using http://apidock.com/rails/Rack/Utils/parse_nested_query:
Rack::Utils.parse_nested_query('item[0][size]')
=> {
"item" => {
"0" => {
"size" => nil
}
}
}
So it's now possible to do:
items_string = item_hash.to_a.map { |row| row.join('=') }.join('&')
result = Rack::Utils.parse_nested_query(items_string)
=> {
"item" => {
"0" => {
"size" => "12",
"count" => "1"
}
}
}

Related

Failed to parse date field [0] with format [MMM, YY] with elastic search 5.0

I am trying to get the date parsed into a string format as month and numerical year format like "JAN, 92". My mapping is as below:
size" => 0,
"query" => {
"bool" => {
"must" => [
{
"term" => {
"checkin_progress_for" => {
"value" => "Goal"
}
}
},
{
"term" => {
"goal_owner_id" => {
"value" => "#{current_user.access_key}"
}
}
}
]
}
},
"aggregations" => {
"chekins_over_time" => {
"range" => {
"field" => "checkin_at",
"format" => "MMM, YY",
"ranges" => [
{
"from" => "now-6M",
"to" => "now"
}
]
},
"aggs" => {
"checkins_monthly" => {
"date_histogram" => {
"field" => "checkin_at",
"format" => "MMM, YY",
"interval" => "month",
"min_doc_count" => 0,
"missing" => 0,
"extended_bounds" => {
"min" => "now-6M",
"max" => "now"
}
}
}
}
}
}
}
I throws the following error:
elasticsearch.transport.RemoteTransportException: [captia-america][127.0.0.1:9300][indices:data/read/search[phase/query]]
Caused by: elasticsearch.ElasticsearchParseException: failed to parse date field [0] with format [MMM, YY]
If I remove the {MMM, YY} and put the normal date format it works.
What could the solution to rectify this.Help appreciated.
Your checkins_monthly aggregation is a bit wrong. The missing part should have the same format for the date to use when the field is missing. A 0 is not actually a date.
For example:
"aggs": {
"checkins_monthly": {
"date_histogram": {
"field": "checkin_at",
"format": "MMM, YY",
"interval": "month",
"min_doc_count": 0,
"missing": "Jan, 17",
"extended_bounds": {
"min": "now-6M",
"max": "now"
}
}
}

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.

Exclude nil values from ElasticSearch Aggregation

I was using this query to retrieve the most significant values:
keywords = Answer.search(
:size => 5,
:query => {
:match => {
:question_id => 32481
}
},
:aggregations => {
:keywords => {
:significant_terms => {
:field => 'text'
}
}
}
)
The field is :text, but it has nil values, so the answer is always:
2.1.2 :135 > keywords.map(&:text)
=> [nil, nil, nil, nil, nil]
I tried to add a filter, as the documentation suggests, but it gives me a parse error:
keywords = Answer.search(
:size => 5,
:query => {
:match => {
:question_id => 32481
},
:filtered => {
:filter => {
:exists => { :field => 'text' }
}
}
},
:aggregations => {
:keywords => {
:significant_terms => {
:field => 'text'
}
}
}
)
I've tried many combinations, with no success. How can I get only the valid text answers?
I believe your ES query should translate to something like this:
"size": 5,
"query": {
"filtered": {
"query": { "match": { "question_id" : 32481 } },
"filter": {
"exists": {
"field": "text"
}
}
}
},
"aggs": {
"keywords": {
"significant_terms": {
"field": "text"
}
}
}
meaning your "question_id" "match" should be enclosed in the "filtered" element.

What is the correct way to chain map_reduce calls in Mongoid?

I have an Element model that belongs to User. I am trying to calculate the following hash: how many users have element count of 1, 2, 3, etc. The approach I take is to first generate a hash of {user -> num elements}, then I sort-of invert it using a second map-reduce.
Here's what I have so far:
Element.map_reduce(%Q{
emit(this.user_id, 1);
}, %Q{
function(key, values) {
return Array.sum(values);
}
}).out(inline: true).map_reduce(%Q{
if (this.value > 1) {
emit(this.value, this._id);
}
}, %Q{
function(element_count, user_ids) {
return user_ids.length;
}
}).out(inline: true)
This gives me an "undefined method `map_reduce'" error. I couldn't find the answer in the docs. Any help would be great.
I calculated the hash using aggregate instead mapreduce, first grouping by user, and then grouping again by elements count:
Element.collection.aggregate([
{
"$group" => {
"_id" => "$user_id", "elements_count" => {"$sum" => 1}
}
},
{
"$group" => {
"_id" => "$elements_count", "users_count" => {"$sum" => 1}
}
},
{ "$project" => {
"_id" => 0,
"users_count" => '$users',
"elements_count" => '$_id',
}
}
])
This returns the following array:
[
{"users_count"=>3, "elements_count"=>2},
{"users_count"=>4, "elements_count"=>3},
...
]
If needed it can also be sorted using $sort operator

Hash remove all except specific keys

I would like to remove every key from a hash except a given key.
For example:
{
"firstName": "John",
"lastName": "Smith",
"age": 25,
"address":
{
"streetAddress": "21 2nd Street",
"city": "New York",
"state": "NY",
"postalCode": "10021"
},
"phoneNumber":
[
{
"type": "home",
"number": "212 555-1234"
},
{
"type": "fax",
"number": "646 555-4567"
}
]
}
I want to remove everything except "firstName" and/or "address".
What about slice?
hash.slice('firstName', 'lastName')
# => { 'firstName' => 'John', 'lastName' => 'Smith' }
Available in Ruby since 2.5
Some other options:
h.select {|k,v| ["age", "address"].include?(k) }
Or you could do this:
class Hash
def select_keys(*args)
select {|k,v| args.include?(k) }
end
end
So you can now just say:
h.select_keys("age", "address")
If you use Rails, please consider ActiveSupport except() method: http://apidock.com/rails/Hash/except
hash = { a: true, b: false, c: nil}
hash.except!(:c) # => { a: true, b: false}
hash # => { a: true, b: false }
Hash#select does what you want:
h = { "a" => 100, "b" => 200, "c" => 300 }
h.select {|k,v| k > "a"} #=> {"b" => 200, "c" => 300}
h.select {|k,v| v < 200} #=> {"a" => 100}
Edit (for comment):
assuming h is your hash above:
h.select {|k,v| k == "age" || k == "address" }
hash = { a: true, b: false, c: nil }
hash.extract!(:c) # => { c: nil }
hash # => { a: true, b: false }
Inspired by Jake Dempsey's answer, this one should be faster for large hashes, as it only peaks explicit keys rather than iterating through the whole hash:
class Hash
def select_keys(*args)
filtered_hash = {}
args.each do |arg|
filtered_hash[arg] = self[arg] if self.has_key?(arg)
end
return filtered_hash
end
end
No Rails needed to get a very concise code:
keys = [ "firstName" , "address" ]
# keys = hash.keys - (hash.keys - keys) # uncomment if needed to preserve hash order
keys.zip(hash.values_at *keys).to_h

Resources