How to parse and loop through JSON sub-children - ruby-on-rails

I have a JSON string:
{
"normal_domains":[{
"urls [
"domain1.com",
"domain2.com"
],
"id":3,
"find":"ama",
"type":"text"
}
],
"premium_domains":[{
"urls":[
"domain3.com",
"domain4.com"
],
"id":1,
"find":"amg",
"type":"text"
}
]
}
I want to output a list for each domain in the hash with corresponding attributes:
Domain type: normal_domains
Domain: domain3.com
ID: 3
Find: ama
-- for each domain --
The code I have is this, but I cannot get it working. It returns NoMethodError: undefined method [] for nil:NilClass:
from_api = '{"normal_domains":[{"urls":["domain1.com","domain2.com"],"id":3,"find":"ama","type":"text"}],"premium_domains":[{"urls":["domain3.com","domain4.com"],"id":1,"find":"amg","type":"text"}]}'
result = JSON.parse from_api
result.each do |child|
loop_index = 0
child.each do |sub_child|
puts "Domain type: #{child}"
puts "Domain: #{sub_child[loop_index]['urls']}"
puts "ID: #{sub_child[loop_index]['id']}"
puts "Find: #{sub_child[loop_index]['find']}"
loop_index += 1
end
end

The hash returned from JSON.parse does not have a .each method.
Imagine your input hash in a more organized way:
{
"normal_domains":[ {
"urls [
"domain1.com",
"domain2.com"
],
"id":3,
"find":"ama",
"type":"text"
}],
"premium_domains":[{
"urls":[
"domain3.com",
"domain4.com"
],
"id":1,
"find":"amg",
"type":"text"
}]
}
You code should be:
result = JSON.parse from_api
result.keys.each do |domain_type|
childArray = result[domain_type]
childArray.each do |child|
urls = child["urls"]
urls.each do |url|
puts "Domain type: #{domain_type}"
puts "Domain: #{url}"
puts "ID: #{child['id']}"
puts "Find: #{child['find']}"
end
end
end

If you want to iterate over an array in a style that is common in C i.e. using array indexes, you should do it like
urls = domain['urls']
(0..urls.length).each do |i|
puts " URL: #{urls[i]}"
end
or at least handle the case when indexing an array element returns nil when the code tries to access data beyond what has been entered in the array. Using array indexes is often unnecessary since iterators can be used.
Using iterators, one does not need the indexes and there is no need to check if the access is beyond the boundaries of a container.
result.each do |key,value|
puts "Domain type: #{key}"
value.each do |domain|
id = domain['id']
find = domain['find']
type = domain['type']
puts " ID: #{id}"
puts " Find: #{find}"
puts " Type: #{type}"
domain['urls'].each do |url|
puts " URL: #{url}"
end
end
end

Related

Ruby: Passing down key/value after transforming objects in array

Given data:
data = [
{"id":14, "sort":1, "content":"9", foo: "2022"},
{"id":14, "sort":4, "content":"5", foo: "2022"},
{"id":14, "sort":2, "content":"1", foo: "2022"},
{"id":14, "sort":3, "content":"0", foo: "2022"},
{"id":15, "sort":4, "content":"4", foo: "2888"},
{"id":15, "sort":2, "content":"1", foo: "2888"},
{"id":15, "sort":1, "content":"3", foo: "2888"},
{"id":15, "sort":3, "content":"3", foo: "2888"},
{"id":16, "sort":1, "content":"8", foo: "3112"},
{"id":16, "sort":3, "content":"4", foo: "3112"},
{"id":16, "sort":2, "content":"4", foo: "3112"},
{"id":16, "sort":4, "content":"9", foo: "3112"}
]
Got the contents concatenated by their sort and ids with:
formatted = data.group_by { |d| d[:id]}.transform_values do |value_array|
value_array.sort_by { |b| b[:sort] }
.map { |c| c[:content] }.join
end
puts formatted
#=> {14=>"9105", 15=>"3134", 16=>"8449"}
I know that foo exists inside value_array but wondering how can I include foo to exist inside the formatted variable so I can map through it to get the desired output or if it's possible?
Desired Output:
[
{"id":14, "concated_value":"9105", foo: "2022"},
{"id":15, "concated_value":"3134", foo: "2888"},
{"id":16, "concated_value":"8449", foo: "3112"}
]
Since :foo is unique to :id. You can do this as follows:
data.group_by {|h| h[:id]}.map do |_,sa|
sa.map(&:dup).sort_by {|h| h.delete(:sort) }.reduce do |m,h|
m.merge(h) {|key,old,new| key == :content ? old + new : old }
end.tap {|h| h[:concated_value] = h.delete(:content) }
end
#=> [
# {"id":14, foo: "2022", "concated_value":"9105"},
# {"id":15, foo: "2888", "concated_value":"3134"},
# {"id":16, foo: "3112", "concated_value":"8449"}
# ]
First we group by id. group_by {|h| h[:id]}
Then we dup the hashes in the groups (so as not to destory the original). map(&:dup)
Then we sort by sort and delete it at the same time. .sort_by {|h| h.delete(:sort) }
Then we merge the groups together and concatenate the content key only.
m.merge(h) {|key,old,new| key == :content ? old + new : old }
Then we just change the key for content to concated_value tap {|h| h[:concated_value] = h.delete(:content) }
We can use first value from value_array to get our :id & :foo values
formatted = data.group_by { |d| d[:id]}.values.map do |value_array|
concated_value = value_array.sort_by { |b| b[:sort] }
.map { |c| c[:content] }.join
value_array.first.slice(:id, :foo)
.merge concated_value: concated_value
end
I think this is a good usecase for reduce, since after grouping you need first to get rid of the ID in the resulting [ID, VALUES] array from group_by and just return a reduced version of the VALUES part - this can all be done without any ActiveSupport etc. dependencies:
data
.group_by{ |d| d[:id] } # Get an array of [ID, [VALUES]]
.reduce([]) do |a, v| # Reduce it into a new empty array
# Append a new hash to the new array
a << {
id: v[1].first[:id], # Just take the ID of the first entry
foo: v[1].first[:foo], # Dito for foo
concatenated: v[1]
.sort_by{ |s| s[:sort] } # now sort all hashes by its sort key
.collect{ |s| s[:content] } # collect the content
.join # and merge it into a string
}
end
Output:
[{:id=>14, :foo=>"2022", :concatenated=>"9105"},
{:id=>15, :foo=>"2888", :concatenated=>"3134"},
{:id=>16, :foo=>"3112", :concatenated=>"8449"}]
EDIT
I had some other approach in mind when i started to write the previous solution, reduce was not really necessary, since the size of the array after group_by does not change, so a map is sufficient.
But while rewriting the code, i was thinking that creating a new hash with all the keys and copying all the values from the first hash within VALUES was a bit too much work, so it would be easier to just reject the overhead keys:
keys_to_ignore = [:sort, :content]
data
.group_by{ |d| d[:id] } # Get an array of [ID, [VALUES]]
.map do |v|
v[1]
.first # Take the first hash from [VALUES]
.merge({'concatenated': v[1] # Insert the concatenated values
.sort_by{ |s| s[:sort] } # now sort all hashes by its sort key
.collect{ |s| s[:content] } # collect the content
.join # and merge it into a string
})
.select { |k, _| !keys_to_ignore.include? k }
end
Output
[{:id=>14, :foo=>"2022", :concatenated=>"9105"},
{:id=>15, :foo=>"2888", :concatenated=>"3134"},
{:id=>16, :foo=>"3112", :concatenated=>"8449"}]
Online demo here
This will work even without Rails:
$irb> formatted = []
$irb> data.sort_by!{|a| a[:sort]}.map {|z| z[:id]}.uniq.each_with_index { |id, index| formatted << {id: id, concated_value: data.map{|c| (c[:id] == id ? c[:content] : nil)}.join, foo: data[index][:foo]}}
$irb> formatted
[{:id=>14, :concated_value=>"9105", :foo=>"2022"},
{:id=>15, :concated_value=>"3134", :foo=>"2888"},
{:id=>16, :concated_value=>"8449", :foo=>"3112"}]
data.sort_by { |h| h[:sort] }.
each_with_object({}) do |g,h| h.update(g[:id]=>{ id: g[:id],
concatenated_value: g[:content].to_s, foo: g[:foo] }) { |_,o,n|
o.merge(concatenated_value: o[:concatenated_value]+n[:concatenated_value]) }
end.values
#=> [{:id=>14, :concatenated_value=>"9105", :foo=>"2022"},
# {:id=>15, :concatenated_value=>"3134", :foo=>"2888"},
# {:id=>16, :concatenated_value=>"8449", :foo=>"3112"}]
This uses the form of Hash#update (aka merge!) that employs a block to determine the values of keys (here the value of :id) that are present in both hashes being merged. See the doc for the description of the three block variables (here _, o and n).
Note the receiver of values (at the end) is the following.
{ 14=>{ :id=>14, :concatenated_value=>"9105", :foo=>"2022" },
15=>{ :id=>15, :concatenated_value=>"3134", :foo=>"2888" },
16=>{ :id=>16, :concatenated_value=>"8449", :foo=>"3112" } }

Rails join array inside map without escaping

I have an array of hashes that I map into a string
Example:
array_of_hashes = [{
:me => 'happy',
:you => 'notsohappy',
:email => [
{"Contact"=>"", "isVerified"=>"1"},
{"Contact"=>"me#example.com", "isVerified"=>"1"},
{"Contact"=>"you#example.com", "isVerified"=>"1"}
]
},{another instance here...}]
Now I want to convert this to a new array that will give me:
["happy", "notsodhappy", "me#example.com", "you#example.com"]
I need to map and reject empty email addresses in the "email" array of hashes.
So far I tried:
array_of_hashes.map{|record| [
record['me'],
record['you'],
record['email'].map { |email| email['Contact']}.reject { |c| c.empty? }.join('", "')
] }
But this returns ["happy", "notsohappy", "me#example.com\", \"you#example.com"]
The quotes are escapes even if I add .html_safe after the .join
In short, it's insisting to keep the joined array a single string. I need it split to separate strings... as many as are in the array.
I need to get rid of these quotes because I am trying to export the array as CSV and so far it's not splitting the email addresses to separate columns.
Suggestions?
array_of_hashes.map do |h|
[h[:me], h[:you]].push(
h[:email].map {|e|e["Contact"]}.reject(&:empty?)
).flatten
end
# => [["happy", "notsohappy", "me#example.com", "you#example.com"], ...]
results = []
array_of_hashes.each do |hash|
single_result = []
single_result << hash[:me]
single_result << hash[:you]
hash[:email].each do |email|
single_result << email["Contact"] if email["Contact"].present?
end
results << single_result
return results
end
This will results : -
2.3.1 :091 > results
=> [["happy", "notsohappy", "me#example.com", "you#example.com"], ["happy", "notsohappy", "me#example.com", "you#example.com"], ["happy", "notsohappy", "me#example.com", "you#example.com"]]

Grouping the array of codes

I have two different types of codes.
AllCodes = [
{
group_name: 'Marked',
group_codes: [
{
code: '1A',
description: 'Marked.'
}
],
.. //next group codes.
}
]
AllCodes = [
{
code: '2',
description: 'Located. Facilities Marked.'
}
.. //next codes.
]
I need to form an array of this format.
[
{
code: '1A',
description: 'example'
},
.. // next code
]
I did it this way, but I do not really like this approach, how can I dry up the code?
def up
Account.all.each do |account|
arrayed_codes = []
account.one_call_center.response_codes_repository_class::AllCodes.collect do |codes|
if response_code[:group_codes]
response_code[:group_codes].each do |group_codes|
arrayed_codes << {
code: group_codes[:code],
description: group_codes[:description]
}
end
else
arrayed_codes << {
code: response_code[:code],
description: response_code[:description]
}
end
end
arrayed_codes.each do |res_code|
ResponseCode.create!(account: account,
one_call_center_id: account.one_call_center.id,
close_code: res_code[:code],
description: res_code[:description],
ticket_types: account.one_call_center.ticket_types.keys)
end
end
end
# obj is an each element of your AllCodes array
codes = AllCodes.inject([]) do |codes_array, obj|
if obj.has_key?(:group_codes)
codes_array += obj[:group_codes]
else
codes_array << obj
end
end
codes_array is an injected array. Iterating over your AllCodes, if current object has_key?(:group_codes), we should take obj[:group_codes] (because it's already an array of needed format), so we merge our codes_array with it: codes_array += obj[:group_codes]. If it doesn't have that key, than it's already hash of needed format. So we just add this element to the array.

String interpolation with subhashes

In my code I want to use string interpolation for an email subject I am generating.
output = "this is my %{title}" % {title: "Text here"}
This works as expected, but is there a way to use hashes inside of hashes and still be able to use string interpolation?
It would be awesome if I could do something like:
output = "this is my %{title.text}" % {title: {text: "text here"}}
In Ruby 2.3, sprintf checks the hash's default value, so you could provide a default_proc to dig up the nested value:
hash = {title: {text: "text here"}}
hash.default_proc = proc { |h, k| h.dig(*k.to_s.split('.').map(&:to_sym)) }
"this is my %{title.text}" % hash
#=> "this is my text here"
Kind of hacky, but it seems to work.
I don't think this is possible with % method. You'd have to use regular Ruby interpolation with "#{}".
I'd also point out that you can use OpenStruct.
title = OpenStruct.new(text: 'text here')
output = "this is my #{title.text}"
It's actually not hard to make this work if you write a simple utility method to "squash" a nested Hash's keys, e.g.:
def squash_hash(hsh, stack=[])
hsh.reduce({}) do |res, (key, val)|
next_stack = [ *stack, key ]
if val.is_a?(Hash)
next res.merge(squash_hash(val, next_stack))
end
res.merge(next_stack.join(".").to_sym => val)
end
end
hsh = { foo: { bar: 1, baz: { qux: 2 } }, quux: 3 }
p squash_hash(hsh)
# => { :"foo.bar" => 1, :"foo.baz.qux" => 2, :quux => 3 }
puts <<END % squash_hash(hsh)
foo.bar: %{foo.bar}
foo.baz.qux: %{foo.baz.qux}
quux: %{quux}
END
# => foo.bar: 1
# foo.baz.qux: 2
# quux: 3

Process nested hash to convert all values to strings

I have the following code which takes a hash and turns all the values in to strings.
def stringify_values obj
#values ||= obj.clone
obj.each do |k, v|
if v.is_a?(Hash)
#values[k] = stringify_values(v)
else
#values[k] = v.to_s
end
end
return #values
end
So given the following hash:
{
post: {
id: 123,
text: 'foobar',
}
}
I get following YAML output
--- &1
:post: *1
:id: '123'
:text: 'foobar'
When I want this output
---
:post:
:id: '123'
:text: 'foobar'
It looks like the object has been flattened and then been given a reference to itself, which causes Stack level errors in my specs.
How do I get the desired output?
A simpler implementation of stringify_values can be - assuming that it is always a Hash. This function makes use of Hash#deep_merge method added by Active Support Core Extensions - we merge the hash with itself, so that in the block we get to inspect each value and call to_s on it.
def stringify_values obj
obj.deep_merge(obj) {|_,_,v| v.to_s}
end
Complete working sample:
require "yaml"
require "active_support/core_ext/hash"
def stringify_values obj
obj.deep_merge(obj) {|_,_,v| v.to_s}
end
class Foo
def to_s
"I am Foo"
end
end
h = {
post: {
id: 123,
arr: [1,2,3],
text: 'foobar',
obj: { me: Foo.new}
}
}
puts YAML.dump (stringify_values h)
#=>
---
:post:
:id: '123'
:arr: "[1, 2, 3]"
:text: foobar
:obj:
:me: I am Foo
Not sure what is the expectation when value is an array, as Array#to_s will give you array as a string as well, whether that is desirable or not, you can decide and tweak the solution a bit.
There are two issues. First: the #values after the first call would always contain an object which you cloned in the first call, so in the end you will always receive a cloned #values object, no matter what you do with the obj variable(it's because of ||= operator in your call). Second: if you remove it and will do #values = obj.clone - it would still return incorrect result(deepest hash), because you are overriding existing variable call after call.
require 'yaml'
def stringify_values(obj)
temp = {}
obj.each do |k, v|
if v.is_a?(Hash)
temp[k] = stringify_values(v)
else
temp[k] = v.to_s
end
end
temp
end
hash = {
post: {
id: 123,
text: 'foobar',
}
}
puts stringify_values(hash).to_yaml
#=>
---
:post:
:id: '123'
:text: foobar
If you want a simple solution without need of ActiveSupport, you can do this in one line using each_with_object:
obj.each_with_object({}) { |(k,v),m| m[k] = v.to_s }
If you want to modify obj in place pass obj as the argument to each_with_object; the above version returns a new object.
If you are as aware of converting values to strings, I would go with monkeypatching Hash class:
class Hash
def stringify_values
map { |k, v| [k, Hash === v ? v.stringify_values : v.to_s] }.to_h
end
end
Now you will be able to:
require 'yaml'
{
post: {
id: 123,
text: 'foobar'
},
arr: [1, 2, 3]
}.stringify_values.to_yaml
#⇒ ---
# :post:
# :id: '123'
# :text: foobar
# :arr: "[1, 2, 3]"
In fact, I wonder whether you really want to scramble Arrays?

Resources