How to do hash arithmetic - ruby

I want to subtract the ackedOns time from the startOn time in the hash to get how many epoch seconds it took to acknowledge the alert.
Here is the code:
url = "https://xyz"
uri = URI(url)
http = Net::HTTP.new(uri.host, 443)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
req = Net::HTTP::Get.new(uri.request_uri)
response = http.request(req)
jsonResponse = JSON.parse(response.body)
#troubelshooting step to see how many total alerts there are
total = jsonResponse['data']['total']
pp total
# create the collected_alerts hash
collected_alerts = { 'hosts'=> [],'dataPoints' => [], 'startOns' => [], 'ackedOns' => [], 'timeToAcks' => [] }
# iterate through the json
jsonResponse['data']['alerts'].each do |alerts|
# store the interested json values into the appropriate hash
collected_alerts['hosts'] << alerts['host']
collected_alerts['dataPoints'] << alerts['dataPoint']
collected_alerts['startOns'] << alerts['startOn']
collected_alerts['ackedOns'] << alerts['ackedOn']
# Calculate mins it took to acknowledge alert and store it in timeToAcks
# Formula is Ack time - Start time which would give us seconds / 60 to give mins.
alerts['timeToAcks'] = alerts['ackedOn'].zip(alerts['startOn']).map{|a,s| a-s/60}
end
pp collected_alerts
CSV.open("data.csv", "wb") {|csv| collected_alerts.to_a.each {|elem| csv << elem} }
Here is the json response that i am trying to parse
{
"status": 200,
"data": {
"total": 3,
"alerts": [
{
"dataPoint": "average",
"ackedBy": "x",
"dataSourceInstance": "Ping50Packets",
"dataSource": "Ping50Packets",
"host": "x",
"endOn": 0,
"ackedOn": 1392218853,
"dataSourceInstanceId": 400554,
"hostId": 1829,
"type": "alert",
"dataSourceId": 560,
"ackedOnLocal": "2014-02-12 07:27:33 PST",
"id": 6862895,
"startOn": 1392197595,
"thresholds": "> 200",
"endOnLocal": "",
"level": "warn",
"ackComment": "Ack.",
"value": "206.00",
"hostDataSourceId": 137481,
"acked": true,
"hostGroups": [{
"alertEnable": true,
"createdOn": 1367604091,
"id": 106,
"parentId": 105,
"description": "",
"appliesTo": "",
"name": "x",
"fullPath": "x"
}],
"startOnLocal": "2014-02-12 01:33:15 PST"
},

something like;
alerts['tta'] = alerts['ackedOns'].zip(alerts['startOns']).map{|a,s| a-s/60}

Based on your data, you simply need to do:
collected_alerts['timeToAcks'] << (alerts['ackedOn'] - alerts['startOn'])/60
Your origin code had a spelling mistake in one of the field names.
You want to push these values into collected_alerts, not the current alert, right?
Alternatively, after your have looped through all the alerts:
ca = collected_alerts
ca['timeToAcks'] = ca['ackedOns'].zip(ca['startOns']).map{ |ack,st| (ack-st)/60 }
Further, instead of looping through the alerts one at a time and pulling out the pieces, I would do this:
alerts = jsonResponse['data']['alerts']
collected_alerts = {}
%w[host dataPoint startOn ackedOn].each do |field|
collected_alerts[field] = alerts.map{ |alert| alert[field] }
end
Or even better:
alert_fields = %w[host dataPoint startOn ackedOn]
collected_alerts = Hash[ alert_fields.map{ |f| [f,alerts.map{|a| a[f] }] } ]
Explained, roughly in execution order.
alert_fields = %w[ … ] — create a variable referencing an array of string values (space-delimited in the source code), i.e. ["ackedOn", "host", … ]
alerts.map{ |a| … } — Create a new array by taking each value in the alerts array, creating a variable named a that references that value, and use the result of the block as the value in the new array.
a[f] — for each alert in the alerts array, look up the key with the value of f. For example, when f is "ackedOn", this looks up the value of a["ackedOn"].
alert_fields.map{ |f| … } — for each element in the array create a variable named f (for 'field') and run the contents of this block. Create a new array where the contents are, for each entry, whatever the final value of the block results in.
[f, … ] — after creating an array of all the values for a particular field in all the alerts, the result of this block is a two element array that is the name of the field followed by these values. For example, [ "ackedOn", [1,3,16,4,44,7] ].
Hash[ … ] — given an array of two-valued arrays, create a hash mapping the first value in each pair to the second value, i.e. { "ackedOn" => [1,3,16,4,44,7] }
This same Hash[…] method alternatively accepts an even number of arguments, pairing each even-numbered argument with the following one.
With examples:
alert_fields = %w[ackedOn host dataPoint startOn]
#=> ["ackedOn", "host", "dataPoint", "startOn" ]
alerts.map{|a| a[f] }
#=> [5,3,4,5,1,6,8,2,…]
[f,alerts.map{|a| a[f] }]
#=> ["ackedOn",[5,3,4,5,1,6,8,2,…]]
alert_fields.map{ |f| [f,alerts.map{|a| a[f] }] }
#=> [
#=> ["host",['foo','bar',…]],
#=> …
#=> ["ackedOn",[5,3,4,5,1,6,8,2,…]]
#=> ]
Hash[ alert_fields.map{ |f| [f,alerts.map{|a| a[f] }] } ]
#=> {
#=> "host" => ['foo','bar',…],
#=> …
#=> "ackedOn" => [5,3,4,5,1,6,8,2,…]
#=> }

Related

Group hash values by key and concatenate values

I need to group a hash by keys and concatenate the values. For example, given this hash:
[
{"name": "FT002", "data": {"2017-11-01": 1392.0}},
{"name": "FT004", "data": {"2017-11-01": 4091.0}},
{"name": "FT002", "data": {"2017-12-01": 1279.0}},
{"name": "FT004", "data": {"2017-12-01": 3249.0}}
]
I want to produce this hash:
[
{"name": "FT002", "data": {"2017-11-01": 1392.0, "2017-12-01": 1279.0}},
{"name": "FT004", "data": {"2017-11-01": 4091.0, "2017-12-01": 3249.0}}
]
Any help would be appreciated.
I tried various iterations of inject, group_by, and merge, but can't seem to get the right result.
You can accomplish this in three short one-liners, first producing a hash mapping names to data, and then producing your desired structure:
data = [
{"name":"FT002","data":{"2017-11-01":1392.0}},
{"name":"FT004","data":{"2017-11-01":4091.0}},
{"name":"FT002","data":{"2017-12-01":1279.0}},
{"name":"FT004","data":{"2017-12-01":3249.0}}
]
hash = Hash.new { |hash,key| hash[key] = {} }
data.each { |name:, data:| hash[name].merge!(data) }
hash = hash.map { |k,v| { name: k, data: v } }
data.group_by { |h| h[:name] }.map do |k,arr|
{ name: k, data: arr.each_with_object({}) { |g,h| h.update(g[:data]) } }
end
#=> [{:name=>"FT002", :data=>{:"2017-11-01"=>1392.0, :"2017-12-01"=>1279.0}},
# {:name=>"FT004", :data=>{:"2017-11-01"=>4091.0, :"2017-12-01"=>3249.0}}]
The first step is to use Enumerable#group_by to produce the following hash.
data.group_by { |h| h[:name] }
#=> {"FT002"=>[
# {:name=>"FT002", :data=>{:"2017-11-01"=>1392.0}},
# {:name=>"FT002", :data=>{:"2017-12-01"=>1279.0}}
# ],
# "FT004"=>[
# {:name=>"FT004", :data=>{:"2017-11-01"=>4091.0}},
# {:name=>"FT004", :data=>{:"2017-12-01"=>3249.0}}
# ]
# }
The second step is to simply manipulate the keys and values of this hash. See Hash#update (aka merge!).
An alternative to the second step is the following.
data.group_by { |h| h[:name] }.map do |k,arr|
{ name: k, data: arr.map { |g| g[:data].flatten }.to_h }
end
Note that this uses Hash#flatten, not Array#flatten.
This should generate the the results you're looking for:
data = [
{"name":"FT002","data":{"2017-11-01":1392.0}},
{"name":"FT004","data":{"2017-11-01":4091.0}},
{"name":"FT002","data":{"2017-12-01":1279.0}},
{"name":"FT004","data":{"2017-12-01":3249.0}}
]
newData = {}
data.each do |x|
newData[x[:name]] = [] unless newData[x[:name]].present?
newData[x[:name]].push x[:data]
end
combined = []
newData.each do |index,value|
dateData = {}
value.each do |dateStuff|
dateStuff.each do |dateIndex, dateValue|
dateData[dateIndex] = dateValue
end
end
values = {"name": index, "data": dateData}
combined.push values
end
combined

Merging hash values in an array of hashes based on key

I have an array of hashes similar to this:
[
{"student": "a","scores": [{"subject": "math","quantity": 10},{"subject": "english", "quantity": 5}]},
{"student": "b", "scores": [{"subject": "math","quantity": 1 }, {"subject": "english","quantity": 2 } ]},
{"student": "a", "scores": [ { "subject": "math", "quantity": 2},{"subject": "science", "quantity": 5 } ] }
]
Is there a simpler way of getting the output similar to this except looping through the array and finding a duplicate and then combining them?
[
{"student": "a","scores": [{"subject": "math","quantity": 12},{"subject": "english", "quantity": 5},{"subject": "science", "quantity": 5 } ]},
{"student": "b", "scores": [{"subject": "math","quantity": 1 }, {"subject": "english","quantity": 2 } ]}
]
Rules for merging duplicate objects:
Students are merged on matching "value" (e.g. student "a", student "b")
Students scores on identical subjects are added (e.g. student a's math scores 2 and 10 become 12 when merged)
Is there a simpler way of getting the output similar to this except looping through the array and finding a duplicate and then combining them?
Not that I know of. IF you explain where this data is comeing form the answer may be different but just based on the Array of Hash objects I think you will haev to iterate and combine.
While it is not elegant you could use a solution like this
arr = [
{"student"=> "a","scores"=> [{"subject"=> "math","quantity"=> 10},{"subject"=> "english", "quantity"=> 5}]},
{"student"=> "b", "scores"=> [{"subject"=> "math","quantity"=> 1 }, {"subject"=> "english","quantity"=> 2 } ]},
{"student"=> "a", "scores"=> [ { "subject"=> "math", "quantity"=> 2},{"subject"=> "science", "quantity"=> 5 } ] }
]
#Group the array by student
arr.group_by{|student| student["student"]}.map do |student_name,student_values|
{"student" => student_name,
#combine all the scores and group by subject
"scores" => student_values.map{|student| student["scores"]}.flatten.group_by{|score| score["subject"]}.map do |subject,subject_values|
{"subject" => subject,
#combine all the quantities into an array and reduce using `+`
"quantity" => subject_values.map{|h| h["quantity"]}.reduce(:+)
}
end
}
end
#=> [
{"student"=>"a", "scores"=>[
{"subject"=>"math", "quantity"=>12},
{"subject"=>"english", "quantity"=>5},
{"subject"=>"science", "quantity"=>5}]},
{"student"=>"b", "scores"=>[
{"subject"=>"math", "quantity"=>1},
{"subject"=>"english", "quantity"=>2}]}
]
I know that you specified your expected result but I wanted to point out that making the output simpler makes the code simpler.
arr.map(&:dup).group_by{|a| a.delete("student")}.each_with_object({}) do |(student, scores),record|
record[student] = scores.map(&:values).flatten.map(&:values).each_with_object(Hash.new(0)) do |(subject,score),obj|
obj[subject] += score
obj
end
record
end
#=>{"a"=>{"math"=>12, "english"=>5, "science"=>5}, "b"=>{"math"=>1, "english"=>2}}
With this structure getting the students is as easy as calling .keys and the scores would be equally as simple. I am thinking something like
above_result.each do |student,scores|
puts student
scores.each do |subject,score|
puts " #{subject.capitalize}: #{score}"
end
end
end
The console out put would be
a
Math: 12
English: 5
Science: 5
b
Math: 1
English: 2
There are two common ways of aggregating values in such instances. The first is to employ the method Enumerable#group_by, as #engineersmnky has done in his answer. The second is to build a hash using the form of the method Hash#update (a.k.a. merge!) that uses a block to resolve the values of keys which are present in both of the hashes being merged. My solution uses the latter approach, not because I prefer it to the group_by, but just to show you a different way it can be done. (Had engineersmnky used update, I would have gone with group_by.)
Your problem is complicated somewhat by the particular data structure you are using. I found that the solution could be simplfied and made easier to follow by first converting the data to a different structure, update the scores, then convert the result back to your data structure. You may want to consider changing the data structure (if that's an option for you). I've addressed that issue in the "Discussion" section.
Code
def combine_scores(arr)
reconstruct(update_scores(simplify(arr)))
end
def simplify(arr)
arr.map do |h|
hash = Hash[h[:scores].map { |g| g.values }]
hash.default = 0
{ h[:student]=> hash }
end
end
def update_scores(arr)
arr.each_with_object({}) do |g,h|
h.update(g) do |_, h_scores, g_scores|
g_scores.each { |subject,score| h_scores[subject] += score }
h_scores
end
end
end
def reconstruct(h)
h.map { |k,v| { student: k, scores: v.map { |subject, score|
{ subject: subject, score: score } } } }
end
Example
arr = [
{ student: "a", scores: [{ subject: "math", quantity: 10 },
{ subject: "english", quantity: 5 }] },
{ student: "b", scores: [{ subject: "math", quantity: 1 },
{ subject: "english", quantity: 2 } ] },
{ student: "a", scores: [{ subject: "math", quantity: 2 },
{ subject: "science", quantity: 5 } ] }]
combine_scores(arr)
#=> [{ :student=>"a",
# :scores=>[{ :subject=>"math", :score=>12 },
# { :subject=>"english", :score=> 5 },
# { :subject=>"science", :score=> 5 }] },
# { :student=>"b",
# :scores=>[{ :subject=>"math", :score=> 1 },
# { :subject=>"english", :score=> 2 }] }]
Explanation
First consider the two intermediate calculations:
a = simplify(arr)
#=> [{ "a"=>{ "math"=>10, "english"=>5 } },
# { "b"=>{ "math"=> 1, "english"=>2 } },
# { "a"=>{ "math"=> 2, "science"=>5 } }]
h = update_scores(a)
#=> {"a"=>{"math"=>12, "english"=>5, "science"=>5}
# "b"=>{"math"=> 1, "english"=>2}}
Then
reconstruct(h)
returns the result shown above.
+ simplify
arr.map do |h|
hash = Hash[h[:scores].map { |g| g.values }]
hash.default = 0
{ h[:student]=> hash }
end
This maps each hash into a simpler one. For example, the first element of arr:
h = { student: "a", scores: [{ subject: "math", quantity: 10 },
{ subject: "english", quantity: 5 }] }
is mapped to:
{ "a"=>Hash[[{ subject: "math", quantity: 10 },
{ subject: "english", quantity: 5 }].map { |g| g.values }] }
#=> { "a"=>Hash[[["math", 10], ["english", 5]]] }
#=> { "a"=>{"math"=>10, "english"=>5}}
Setting the default value of each hash to zero simplifies the update step, which follows.
+ update_scores
For the array of hashes a that is returned by simplify, we compute:
a.each_with_object({}) do |g,h|
h.update(g) do |_, h_scores, g_scores|
g_scores.each { |subject,score| h_scores[subject] += score }
h_scores
end
end
Each element of a (a hash) is merged into an initially-empty hash, h. As update (same as merge!) is used for the merge, h is modified. If both hashes share the same key (e.g., "math"), the values are summed; else subject=>score is added to h.
Notice that if h_scores does not have the key subject, then:
h_scores[subject] += score
#=> h_scores[subject] = h_scores[subject] + score
#=> h_scores[subject] = 0 + score (because the default value is zero)
#=> h_scores[subject] = score
That is, the key-value pair from g_scores is merely added to h_scores.
I've replaced the block variable representing the subject with a placeholder _, to reduce the chance of errors and to inform the reader that it is not used in the block.
+ reconstruct
The final step is to convert the hash returned by update_scores back to the original data structure, which is straightforward.
Discussion
If you change the data structure, and it meets your requirements, you may wish to consider changing it to that produced by combine_scores:
h = { "a"=>{ math: 10, english: 5 }, "b"=>{ math: 1, english: 2 } }
Then to update the scores with:
g = { "a"=>{ math: 2, science: 5 }, "b"=>{ english: 3 }, "c"=>{ science: 4 } }
you would merely to the following:
h.merge(g) { |_,oh,nh| oh.merge(nh) { |_,ohv,nhv| ohv+nhv } }
#=> { "a"=>{ :math=>12, :english=>5, :science=>5 },
# "b"=>{ :math=> 1, :english=>5 },
# "c"=>{ :science=>4 } }

categorize by hash value

I have an array of hashes with values like:
by_person = [{ :person => "Jane Smith", :filenames => ["Report.pdf", "File2.pdf"]}, {:person => "John Doe", :filenames => ["Report.pdf] }]
I would like to end up with another array of hashes (by_file) that has each unique value from the filenames key as a key in the by_file array:
by_file = [{ :filename => "Report.pdf", :people => ["Jane Smith", "John Doe"] }, { :filename => "File2.pdf", :people => [Jane Smith] }]
I have tried:
by_file = []
by_person.each do |person|
person[:filenames].each do |file|
unless by_file.include?(file)
# list people that are included in file
by_person_each_file = by_person.select{|person| person[:filenames].include?(file)}
by_person_each_file.each do |person|
by_file << {
:file => file,
:people => person[:person]
}
end
end
end
end
as well as:
by_file.map(&:to_a).reduce({}) {|h,(k,v)| (h[k] ||= []) << v; h}
Any feedback is appreciated, thanks!
Doesn't seem too tricky, but the way you're compiling it isn't very efficient:
by_person = [{ :person => "Jane Smith", :filenames => ["Report.pdf", "File2.pdf"]}, {:person => "John Doe", :filenames => ["Report.pdf"] }]
by_file = by_person.each_with_object({ }) do |entry, index|
entry[:filenames].each do |filename|
set = index[filename] ||= [ ]
set << entry[:person]
end
end.collect do |filename, people|
{
filename: filename,
people: people
}
end
puts by_file.inspect
# => [{:filename=>"Report.pdf", :people=>["Jane Smith", "John Doe"]}, {:filename=>"File2.pdf", :people=>["Jane Smith"]}]
This makes use of a hash to group the people by filename, essentially inverting your structure, and then converts that into the final format in a second pass. This is more efficient than working with the final format during compilation as that's not indexed and requires an expensive linear search to find the correct container to insert into.
An alternate method is to create a default hash constructor that makes the structure you're looking for:
by_file_hash = Hash.new do |h,k|
h[k] = {
filename: k,
people: [ ]
}
end
by_person.each do |entry|
entry[:filenames].each do |filename|
by_file_hash[filename][:people] << entry[:person]
end
end
by_file = by_file_hash.values
puts by_file.inspect
# => [{:filename=>"Report.pdf", :people=>["Jane Smith", "John Doe"]}, {:filename=>"File2.pdf", :people=>["Jane Smith"]}]
This may or may not be easier to understand.
This is one way to do it.
Code
def convert(by_person)
by_person.each_with_object({}) do |hf,hp|
hf[:filenames].each do |fname|
hp.update({ fname=>[hf[:person]] }) { |_,oh,nh| oh+nh }
end
end.map { |fname,people| { :filename => fname, :people=>people } }
end
Example
by_person = [{:person=>"Jane Smith", :filenames=>["Report.pdf", "File2.pdf"]},
{:person=>"John Doe", :filenames=>["Report.pdf"]}]
convert(by_person)
#=> [{:filename=>"Report.pdf", :people=>["Jane Smith", "John Doe"]},
# {:filename=>"File2.pdf", :people=>["Jane Smith"]}]
Explanation
For by_person in the example:
enum1 = by_person.each_with_object({})
#=>[{:person=>"Jane Smith", :filenames=>["Report.pdf", "File2.pdf"]},
{:person=>"John Doe", :filenames=>["Report.pdf"]}]:each_with_object({})>
Let's see what values the enumerator enum will pass into the block:
enum1.to_a
#=> [[{:person=>"Jane Smith", :filenames=>["Report.pdf", "File2.pdf"]}, {}],
# [{:person=>"John Doe", :filenames=>["Report.pdf"]}, {}]]
As will be shown below, the empty hash in the first element of the enumerator will no longer be empty with the second element is passed into the block.
The first element is assigned to the block variables as follows (I've indented to indicate the block level):
hf = {:person=>"Jane Smith", :filenames=>["Report.pdf", "File2.pdf"]}
hp = {}
enum2 = hf[:filenames].each
#=> #<Enumerator: ["Report.pdf", "File2.pdf"]:each>
enum2.to_a
#=> ["Report.pdf", "File2.pdf"]
"Report.pdf" is passed to the inner block, assigned to the block variable:
fname = "Report.pdf"
and
hp.update({ "Report.pdf"=>["Jane Smith"] }) { |_,oh,nh| oh+nh }
#=> {"Report.pdf"=>["Jane Smith"]}
is executed, returning the updated value of hp.
Here the block for Hash#update (aka Hash#merge!) is not consulted. It is only needed when the hash hp and the merging hash (here { fname=>["Jane Smith"] }) have one or more common keys. For each common key, the key and the corresponding values from the two hashes are passed to the block. This is elaborated below.
Next, enum2 passes "File2.pdf" into the block and assigns it to the block variable:
fname = "File2.pdf"
and executes
hp.update({ "File2.pdf"=>["Jane Smith"] }) { |_,oh,nh| oh+nh }
#=> {"Report.pdf"=>["Jane Smith"], "File2.pdf"=>["Jane Smith"]}
which returns the updated value of hp. Again, update's block was not consulted. We're now finished with Jane, so enum1 next passes its second and last value into the block and assigns the block variables as follows:
hf = {:person=>"John Doe", :filenames=>["Report.pdf"]}
hp = {"Report.pdf"=>["Jane Smith"], "File2.pdf"=>["Jane Smith"]}
Note that hp has now been updated. We then have:
enum2 = hf[:filenames].each
#=> #<Enumerator: ["Report.pdf"]:each>
enum2.to_a
#=> ["Report.pdf"]
enum2 assigns
fname = "Report.pdf"
and executes:
hp.update({ "Report.pdf"=>["John Doe"] }) { |_,oh,nv| oh+nv }
#=> {"Report.pdf"=>["Jane Smith", "John Doe"], "File2.pdf"=>["Jane Smith"]}
In making this update, hp and the hash being merged both have the key "Report.pdf". The following values are therefore passed to the block variables |k,ov,nv|:
k = "Report.pdf"
oh = ["Jane Smith"]
nh = ["John Doe"]
We don't need the key, so I've replaced it with an underscore. The block returns
["Jane Smith"]+["John Doe"] #=> ["Jane Smith", "John Doe"]
which becomes the new value for the key "Report.pdf".
Before turning to the final step, I'd like to suggest that you consider stopping here. That is, rather than constructing an array of hashes, one for each file, just leave it as a hash with the files as keys and arrays of persons the values:
{ "Report.pdf"=>["Jane Smith", "John Doe"], "File2.pdf"=>["Jane Smith"] }
The final step is straightforward:
hp.map { |fname,people| { :filename => fname, :people=>people } }
#=> [{ :filename=>"Report.pdf", :people=>["Jane Smith", "John Doe"] },
# { :filename=>"File2.pdf", :people=>["Jane Smith"] }]

Fill array with element N times

I want to fill an array with 1 element but 5 times.
What I got so far.
number = 1234
a = []
5.times { a << number }
puts a # => 1234, 1234, 1234, 1234, 1234
It works but this feels not the ruby way.
Can someone point me in the right direction to init an array with 5 times the same value?
For immutable objects like Fixnums etc
Array.new(5, 1234) # Assigns the given instance to each item
# => [1234, 1234, 1234, 1234, 1234]
For Mutable objects like String Arrays
Array.new(5) { "Lorem" } # Calls block for each item
# => ["Lorem", "Lorem", "Lorem", "Lorem", "Lorem"]
This should work:
[1234] * 5
# => [1234, 1234, 1234, 1234, 1234]
Although the accepted answer is fine in the case of strings and other immutable objects, I think it's worth expanding on Max's comment about mutable objects.
The following will fill an array of elements with 3 individually instantiated hashes:
different_hashes = Array.new(3) { {} } # => [{}, {}, {}]
The following will fill an array of elements with a reference to the same hash 3 times:
same_hash = Array.new(3, {}) # => [{}, {}, {}]
If you modify the first element of different_hashes:
different_hashes.first[:hello] = "world"
Only the first element will be modified.
different_hashes # => [{ hello: "world" }, {}, {}]
On the other hand, if you modify the first element of same_hash, all three elements will be modified:
same_hash.first[:hello] = "world"
same_hash # => [{ hello: "world" }, { hello: "world" }, { hello: "world" }]
which is probably not the intended result.
You can fill the array like this:
a = []
=> []
a.fill("_", 0..5) # Set given range to given instance
=> ["_", "_", "_", "_", "_"]
a.fill(0..5) { "-" } # Call block 5 times
these two methods will give an array of N values in ruby
Array.new(5, 2) => [2,2,2,2,2]
[2] * 5 => [2,2,2,2,2]

Summing Nested Json

So, we have a json response like:
Link to Formatted Sample Json
{"C":{"1":{"1":{"A":[18],"B":[18],"C":[20],"D":[24],"E":[24],"F":[2],"G":[15],"H":[21],"I":[8]},"2":{"A":[9],"B":[26],"C":[12],"D":[10],"E":[10],"F":[3],"G":[7]},"3":{"A":[6],"B":[4],"C":[5],"D":[3],"E":[4],"F":[13]},"4":{"A":[3],"B":[2],"C":[5],"D":[13],"E":[5],"F":[5],"G":[4],"H":[7]},"5":{"A":[10],"B":[10],"C":[10],"D":[10],"E":[10],"F":[15]},"6":{"A":[10],"B":[7],"C":[5],"D":[4],"E":[7],"F":[10],"G":[4],"H":[18]},"7":{"A":[2],"B":[18],"C":[6],"D":[3],"E":[2],"F":[5],"G":[7],"H":[5],"I":[17]},"8":{"A":[20],"B":[2],"C":[10],"D":[3],"E":[5],"F":[10]},"Review 1":{"A":[30]},"Review 2":{"A":[30]}},"2":{"1":{"A":[2],"B":[3],"C":[10],"D":[10],"E":[10],"F":[15]},"10":{"A":[10],"B":[3],"C":[3],"D":[3],"E":[20]},"11":{"A":[2],"B":[6],"C":[5],"D":[10],"E":[10],"F":[13]},"2":{"A":[5],"B":[5],"C":[5],"D":[6],"E":[6],"F":[12],"G":[6],"H":[8]},"3":{"A":[3],"B":[4],"C":[8],"D":[3],"E":[2],"F":[3],"G":[12]},"4":{"A":[10],"B":[10],"C":[10],"D":[11],"E":[10],"F":[20]},"5":{"A":[8],"B":[4],"C":[8],"D":[5],"E":[14]},"6":{"A":[5],"B":[10],"C":[14],"D":[14]},"7":{"A":[3],"B":[5],"C":[8],"D":[9],"E":[10],"F":[16]},"8":{"A":[2],"B":[2],"C":[4],"D":[2],"E":[3],"F":[6],"G":[8]},"9":{"A":[2],"B":[6],"C":[5],"D":[11]},"_mex":{"1":[9]},"Review 1":{"A":[31]},"Review 2":{"A":[30]},"Review 3":{"A":[30]}},"3":{"1":{"A":[1],"B":[1],"C":[1],"D":[2],"E":[6]},"2":{"A":[2],"B":[4],"C":[7],"D":[8],"E":[8],"F":[9]},"3":{"A":[5],"B":[8],"C":[11]},"4":{"A":[10],"B":[10],"C":[11]},"5":{"A":[2],"B":[4],"C":[5],"D":[1],"E":[3],"F":[8]},"6":{"A":[4],"B":[8],"C":[8],"D":[12],"E":[8],"F":[20]},"7":{"A":[25],"B":[12],"C":[13],"D":[15],"E":[12],"F":[20]},"8":{"A":[5],"B":[3],"C":[3],"D":[7],"E":[1],"F":[1],"G":[1],"H":[1],"I":[1],"J":[3],"K":[17]},"mex2":{"A":[7]},"_mex2":{"A":[7]},"Review 1":{"A":[30]},"Review 2":{"A":[30]}},"4":{"1":{"A":[10],"B":[2],"C":[2],"D":[8],"E":[3],"F":[3]},"2":{"A":[5],"B":[10],"C":[5],"D":[10],"E":[10]},"3":{"A":[6],"B":[4],"C":[3],"D":[11]},"4":{"A":[4],"B":[4],"C":[4],"D":[4],"E":[11],"F":[21]},"5":{"A":[5],"B":[8],"C":[3],"D":[4],"E":[5],"F":[7],"G":[15],"H":[5],"I":[5],"J":[6],"K":[14]},"6":{"A":[2],"B":[4],"C":[3],"D":[2],"E":[2],"F":[3],"G":[4],"H":[4],"I":[4],"J":[4],"K":[7],"L":[34]},"_mex2":{"A":[7]},"Review 1":{"A":[77]}}}}
What I want to do is sum all the numbers contained in the response.
Ive tried iterating through all the nesting but I was only been able to do one section. Using:
#number = 0
json["C"]["1"]["1"].each do |key, val|
val.map do |x|
#number+=x
end
end
#=> 150
Any suggestions how I would do that same for json["C"]["1"]?
Based on the JSON, here's code that'll walk the hash:
hash = JSON.parse(json)
def sum_hash(h)
sum = 0
h.each do |k, v|
sum += v.is_a?(Hash) ? sum_hash(v) : v.first
end
sum
end
sum_hash(hash) # => 1964
The hash has to be walked, and each value inspected since it's irregular. If the value is another hash sum_hash calls itself with that sub-hash, which then begins walking the sub-hash received.
For each hash value that isn't a hash, the integer is retrieved from the array using first and added to sum. When the method exits it returns the current value of sum, so, once the hash has been descended into, successive sum values get added.
Reducing the JSON makes it a LOT easier to make sure the code is doing the right thing:
json = '
{
"C": {
"1": {
"1": {
"A": [1],
"B": [1]
},
"2": {
"A": [1],
"B": [1]
},
"Review 1": {
"A": [1]
},
"Review 2": {
"A": [1]
}
}
}
}
'
Running the above code with that says the sum is 6.

Resources