Hash modification issue in hash, replacing value in ruby - ruby-on-rails

I would like to get rid of the value: <value> key value inside each of the attributes in the hash. And make it like this: "total_interactions": 493.667
Below is the incorrect format followed by the expected good format I hope to achieve in json.
{
"3": {
"total_interactions": {
"value": 493.667
},
"shares": {
"value": 334
},
"comments": {
"value": 0
},
"likes": {
"value": 159.66666666666666
},
"total_documents": 6
},
"4": {
"total_interactions": {
"value": 701
},
"shares": {
"value": 300
},
"comments": {
"value": 0
},
"likes": {
"value": 401
},
"total_documents": 1
}
}
I want it to be like this:
{
"3": {
"total_interactions": 493.6666666666667,
"shares": 334,
"comments": 0,
"likes": 159.66666666666666,
"total_documents": 6
},
"4": {
"total_interactions": 701,
"shares": 300,
"comments": 0,
"likes": 401,
"total_documents": 1
}
}
Here is the code that is supposed to do this but is not working. Nothing affected. Not sure what is wrong
# the result_hash variable is the first hash with value: <value>
result_hash.each do |hash_item|
hash_item.each do |key,value_hash|
if( !value_hash.nil? )
value_hash.each do |k,v|
hash_item[key] = v
end
end
end
end

Max Williams' code is perfect for in-place. You can also do this in functional style to get a new, corrected hash:
hash.merge(hash) {|k,v| v.merge(v) {|kk,vv| vv.is_a?(Hash) && vv['value'] ? vv['value'] : vv }}

hash = {"3"=>{"total_documents"=>6, "comments"=>{"value"=>0}, "total_interactions"=>{"value"=>493.667}, "shares"=>{"value"=>334}, "likes"=>{"value"=>159.666666666667}},
"4"=>{"total_documents"=>1, "comments"=>{"value"=>0}, "total_interactions"=>{"value"=>701}, "shares"=>{"value"=>300}, "likes"=>{"value"=>401}}}
hash.each do |k,v|
v.each do |k2, v2|
if v2.is_a?(Hash) && v2["value"]
hash[k][k2] = v2["value"]
end
end
end
after this:
hash = {"3"=>{"total_documents"=>6, "comments"=>0, "total_interactions"=>493.667, "shares"=>334, "likes"=>159.666666666667},
"4"=>{"total_documents"=>1, "comments"=>0, "total_interactions"=>701, "shares"=>300, "likes"=>401}}

If you do not wish to mutate your initial hash, h,you can do this:
h.each_with_object({}) { |(k,v),g|
g[k] = v.each_with_object({}) { |(kk,vv),f|
f[kk] = (Hash === vv) ? vv[:value] : vv } }
#=> {:"3"=>{:total_interactions=>493.667,
# :shares=>334,
# :comments=>0,
# :likes=>159.66666666666666,
# :total_documents=>6},
# :"4"=>{:total_interactions=>701,
# :shares=>300,
# :comments=>0,
# :likes=>401,
# :total_documents=>1}}

Related

Merge object and add keys from JSON format using Ruby on Rails

I have a list of array that is queried that needs to be merged with the same location_id based on the objects.
**this are the code for generating array **
filled = Product.on_hand_location(pid).to_a
empty = Product.on_hand_location_empty_cylinder(pid).to_a
data = filled + empty
result = data.map{ |k|
{
details: {
location_id: k['location_id'],
"location_name"=>k['location_name'],
"onhandcylynder"=>k['onhand'] == nil ? 0 : k['onhand'],
"emptycylynder"=> k['emptyonhand'] == nil ? 0 : k['emptyonhand']
} ,
}
}
respond_with [ onhand: result ]
This JSON format below is the output of code above. which has location_id that needs to be merge
[{
"onhand": [{
"details": {
"location_id": 1,
"location_name": "Capitol Drive",
"onhandcylynder": "4.0",
"emptycylynder": 0
}
},
{
"details": {
"location_id": 2,
"location_name": "SM City Butuan",
"onhandcylynder": "5.0",
"emptycylynder": 0
}
},
{
"details": {
"location_id": 1,
"location_name": null,
"onhandcylynder": 0,
"emptycylynder": "2.0"
}
}
]
}]
My desired output
[{
"onhand": [{
"details": {
"location_id": 1,
"location_name": "Capitol Drive",
"onhandcylynder": "4.0",
"emptycylynder": 0
}
},
{
"details": {
"location_id": 2,
"location_name": "SM City Butuan",
"onhandcylynder": "5.0",
"emptycylynder": "2.0"
}
}
]
}]
I think instead of data = filled + empty you should try
data = Product.on_hand_location(pid).to_a
empty = Product.on_hand_location_empty_cylinder(pid).to_a
empty.each do |location|
data.push(location) if data.none? { |item| item['location_id'] == product['location_id'] }
end
result = ...
or
hash = {}
Product.on_hand_location_empty_cylinder(pid).map { |l| hash[l['location_id']] = l }
Product.on_hand_location(pid).map { |l| hash[l['location_id']] = l }
data = hash.values
and if you need some data from both of the queries you should try
hash = {}
Product.on_hand_location_empty_cylinder(pid).map { |l| hash[l['location_id']] = l }
Product.on_hand_location(pid).map { |l| hash[l['location_id']].merge!(l) }
data = hash.values
to merge in place
I refactor my code and able to get my desired result
filled = Product.on_hand_location(pid).to_a
emptyTank = Product.on_hand_location_empty_cylinder(pid).to_a
data = filled + emptyTank
cylinder = data.map(&:dup)
.group_by { |e| e.delete('location_id') }
finalResult = cylinder.map{|_, location_id | location_id.reduce({}) { |result, location_id|
result.merge(location_id)
} }
respond_with [ onhand: finalResult]
The result already merge with the same location id and emptyonhand keys are merge already in corresponding to its location ID
[
{
"onhand": [
{
"onhand": "1.0",
"location_name": "Capitol Drive",
"emptyonhand": "5.0"
},
{
"onhand": "5.0",
"location_name": "SM City Butuan"
}
]
}
]

How to map ruby hashes correctly based on key provided

My data is like:
h = { themes_data: {
Marketing: [
{
id: 68,
projectno: "15",
}
],
Produktentwicklung: [
{
id: 68,
projectno: "15",
},
{
id: 4,
projectno: "3",
}
],
Marketing_summary: [
{
ges: {
result: "47.6"
},
theme: "Marketing"
}
],
Produktentwicklung_summary: [
{
ges: {
result: "87.7"
},
theme: "Produktentwicklung"
}
]
}
}
And my output should be like:
{ "marketing" => [
{
id: 68,
projectno: "15",
},
{
ges: {
result: "47.6"
},
theme: "Marketing"
}
],
"Produktentwicklung" => [
{
id: 68,
projectno: "15"
},
{
id: 4,
projectno: "3",
},
{
ges: {
result: "87.7"
},
theme: "Produktentwicklung"
}
]
}
Code:
def year_overview_theme
branch_hash = {}
#themes_data.each do |td|
arr = []
td[1].map do |dt|
arr << [{content: dt[:projectno], size: 5, align: :right, background_color: 'D8E5FF'}]
end
branch_hash["#{td[0]}"] = arr
end
branch_hash
end
The problem is that it does not iterate for right hash key.
For example, i want like:
marketing + marketing_summary as 1 hash and similarly
Produktentwicklung = Produktentwicklung_summary as one hash but there is some problem in my logic.
Is there a way that I can check like after 2 iteration,
it should do arr << data with branch_hash["#{td[0]}"] = arr ?
The desired hash can be constructed as follows.
h[:themes_data].each_with_object({}) { |(k,v),g|
g.update(k.to_s[/[^_]+/]=>v) { |_,o,n| o+n } }
#=> { "Marketing"=>[
# {:id=>68, :projectno=>"15"},
# {:ges=>{:result=>"47.6"}, :theme=>"Marketing"}
# ],
# "Produktentwicklung"=>[
# {:id=>68, :projectno=>"15"},
# {:id=>4, :projectno=>"3"},
# {:ges=>{:result=>"87.7"}, :theme=>"Produktentwicklung"}
# ]
# }
This uses the form of Hash#update (aka merge) that employs a block to determine the values of keys that are present in both hashes being merged. Here that block is:
{ |_,o,n| o+n }
The first block variable, _, is the common key. I have represented it with an underscore (a valid local variable) to tell the reader that it is not used in the block calculation. That is common practice. The values of the other two block variables, o and n, are explained at the link for the method update.
The regular expression /[^_]+/, matches one or more characters from the start of the string that are not (^) underscores. When used with the method String#[], we obtain:
"Marketing"[/[^_]+/] #=> "Marketing"
"Marketing_summary"[/[^_]+/] #=> "Marketing"
Let me start with a note: This looks to me like something that should rather be solved in SQL (if it's coming from SQL) instead of Ruby.
With that out of the way, here's a solution that should work:
output = {}
themes_data.each do |theme, projects|
projects.each do |project|
key = project[:theme] || theme.to_s
output[key] ||= [] # make sure the target is initialized
output[key] << project
end
end
There would probably be more elegant solutions using reduce or each_with_object but this works and it's simple enough.
keys = themes_data.keys
summary_keys = themes_data.keys.grep(/_summary/)
result = {}.tap do |hash|
(keys - summary_keys).each do |key|
hash[key] = themes_data[key] + themes_data["#{key}_summary".to_sym]
end
end

How to remove multiple attributes from a json using ruby

I have a json object. It has multiple fields "passthrough_fields" which is unnecessary for me and I want to remove them. Is there a way to get all those attributes filtered out?
JSON:
{
"type": "playable_item",
"id": "p06s0lq7",
"urn": "urn:bbc:radio:episode:p06s0mk3",
"network": {
"id": "bbc_radio_five_live",
"key": "5live",
"short_title": "Radio 5 live",
"logo_url": "https://sounds.files.bbci.co.uk/v2/networks/bbc_radio_five_live/{type}_{size}.{format}",
"passthrough_fields": {}
},
"titles": {
"primary": "Replay",
"secondary": "Bill Shankly",
"tertiary": null,
"passthrough_fields": {}
},
"synopses": {
"short": "Bill Shankly with Sue MacGregor in 1979 - five years after he resigned as Liverpool boss.",
"medium": null,
"long": "Bill Shankly in conversation with Sue MacGregor in 1979, five years after he resigned as Liverpool manager.",
"passthrough_fields": {}
},
"image_url": "https://ichef.bbci.co.uk/images/ic/{recipe}/p06qbz1x.jpg",
"duration": {
"value": 1774,
"label": "29 mins",
"passthrough_fields": {}
},
"progress": null,
"container": {
"type": "series",
"id": "p06qbzmj",
"urn": "urn:bbc:radio:series:p06qbzmj",
"title": "Replay",
"synopses": {
"short": "Colin Murray unearths classic sports commentaries and interviews from the BBC archives.",
"medium": "Colin Murray looks back at 90 years of sport on the BBC by unearthing classic commentaries and interviews from the BBC archives.",
"long": null,
"passthrough_fields": {}
},
"activities": [],
"passthrough_fields": {}
},
"availability": {
"from": "2018-11-16T16:18:54Z",
"to": null,
"label": "Available for over a year",
"passthrough_fields": {}
},
"guidance": {
"competition_warning": false,
"warnings": null,
"passthrough_fields": {}
},
"activities": [],
"uris": [
{
"type": "latest",
"label": "Latest",
"uri": "/v2/programmes/playable?container=p06qbzmj&sort=sequential&type=episode",
"passthrough_fields": {}
}
],
"passthrough_fields": {}
}
Is there a way I can remove all those fields and store the updated json in a new variable?
You can do this recursively to tackle nested occurances of passthrough_fields, whether they're found in an array or a sub hash. Inline comments to explain things a little as it goes:
hash = JSON.parse(input) # convert the JSON to a hash
def remove_recursively(hash, *to_remove)
hash.each do |key, val|
hash.except!(*to_remove) # the heavy lifting: remove all keys that match `to_remove`
remove_recursively(val, *to_remove) if val.is_a? Hash # if a nested hash, run this method on it
if val.is_a? Array # if a nested array, loop through this checking for hashes to run this method on
val.each { |el| remove_recursively(el, *to_remove) if el.is_a? Hash }
end
end
end
remove_recursively(hash, 'passthrough_fields')
To demonstrate, with a simplified example:
hash = {
"test" => { "passthrough_fields" => [1, 2, 3], "wow" => '123' },
"passthrough_fields" => [4, 5, 6],
"array_values" => [{ "to_stay" => "I am", "passthrough_fields" => [7, 8, 9]}]
}
remove_recursively(hash, 'passthrough_fields')
#=> {"test"=>{"wow"=>"123"}, "array_values"=>[{"to_stay"=>"I am"}]}
remove_recursively(hash, 'passthrough_fields', 'wow', 'to_stay')
#=> {"test"=>{}, "array_values"=>[{}]}
This will tackle any arrays, and will dig for nested hashes however deep it needs to go.
It takes any number of fields to remove, in this case a single 'passthrough_fields'.
Hope this helps, let me know how you get on.
I think that the easiest solution would be to:
convert JSON into hash (JSON.parse(input))
use this answer to extend the functionality of Hash (save it in config/initializers/except_nested.rb)
on the hash from 1st step, call:
without_passthrough = your_hash.except_nested('passthrough_fields')
covert hash to JSON (without_passthrough.to_json)
Please keep in mind that it will work for passthrough_fields that is nested directly in hashes. In your JSON, you have the following part:
"uris" => [
{
"type"=>"latest",
"label"=>"Latest",
"uri"=>"/v2/programmes/playable?container=p06qbzmj&sort=sequential&type=episode",
"passthrough_fields"=>{}
}
]
In this case, the passthrough_fields will not be removed. You have to find a more sophisticated solution :)
You can do something like this:
def nested_except(hash, except_key)
sanitized_hash = {}
hash.each do |key, value|
next if key == except_key
sanitized_hash[key] = value.is_a?(Hash) ? nested_except(value, except_key) : value
end
sanitized_hash
end
json = JSON.parse(json_string)
sanitized = nested_except(json, 'passthrough_fields')
See example:
json = { :a => 1, :b => 2, :c => { :a => 1, :b => { :a => 1 } } }
nested_except(json, :a)
# => {:b=>2, :c=>{:b=>{}}}
This helper can easily be converted to support multiple keys to except, simply by except_keys = Array.wrap(except_key) and next if except_keys.include?(key)

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 get the union of hashes in ruby for this json structure

Below is json I translated from ruby hash for ease of representation for this question using hash.to_json. Notice how the key range is being repeated since the values in the nested doc are different. How do I merge the ranges so that for the weight key both "gt": 2232, "lt": 4444 fall under the one hash key weight inside range. Is there some union or collapse method in ruby to sort of "compactify" hashes?
{
"must": [
{
"match": {
"status_type": "good"
}
},
{
"range": {
"created_date": {
"lte": 43252
}
}
},
{
"range": {
"created_date": {
"gt": "42323"
}
}
},
{
"range": {
"created_date": {
"gte": 523432
}
}
},
{
"range": {
"weight": {
"gt": 2232
}
}
},
{
"range": {
"weight": {
"lt": 4444
}
}
}
],
"should": [
{
"match": {
"product_age": "old"
}
}
]
}
Want to change the above to this:
{
"must": [
{
"range": {
"created_date": {
"gte": 523432,
"gt": "42323"
}
}
},
{
"range": {
"weight": {
"gt": 2232,
"lt": 4444
}
}
}
],
"should": [
{
"match": {
"product_age": "old"
}
}
]
}
I don't know of a built in way to handle something like this, but you could write a method that does something like this:
def collapse(array, key)
# Get only the hashes with :range
to_collapse = array.select { |elem| elem.has_key? key }
uncollapsed = array - to_collapse
# Get the hashes that :range points to
to_collapse = to_collapse.map { |elem| elem.values }.flatten
collapsed = {}
# Iterate through each range hash and their subsequent subhashes.
# Collapse the values into the collapsed hash as necessary
to_collapse.each do |elem|
elem.each do |k, v|
collapsed[k] = {} unless collapsed.has_key? k
v.each do |inner_key, inner_val|
collapsed[k][inner_key] = inner_val
end
end
end
[uncollapsed, collapsed].flatten
end
hash[:must] = collapse hash[:must], :range
Note that this is a specific solution that's mainly applicable to the presented problem. It only works for the hash/array depths specified here. You could probably write a recursive solution that could potentially work at any level of depth with a bit more work.

Resources