Ruby aggregate selective values within a collection of hashes - ruby-on-rails

I have an array of hashes with the keys being countries and the values being number of days.
I would like to aggregate over the hashes and sum the values for the countries that are the same.
the array could look like this countries = [{"Country"=>"Brazil", "Duration"=>731/1 days}, {"Country"=>"Brazil", "Duration"=>365/1 days}]
I would like this to return something on the lines of: [{"Country" => "Brazil", "Duration"=>1096/1 days}]
I tried the other questions on SO like this one
countries.inject{|new_h, old_h| new_h.merge(old_h) {|_, old_v, new_v| old_v + new_v}}
Produces {"Country"=>"BrazilBrazil", "Duration"=>1096/1 days}
Is there a way to selectively only merge specific values?

This uses the form of Hash::new that creates a creates an empty hash with a default value (here 0). For a hash h created that way, h[k] returns the default value if the hash does not have a key k. The hash is not modified.
countries = [{"Country"=>"Brazil", "Duration"=>"731/1 days"},
{"Country"=>"Argentina", "Duration"=>"123/1 days"},
{"Country"=>"Brazil", "Duration"=>"240/1 days"},
{"Country"=>"Argentina", "Duration"=>"260/1 days"}]
countries.each_with_object(Hash.new(0)) {|g,h| h[g["Country"]] += g["Duration"].to_i }.
map { |k,v| { "Country"=>k, "Duration"=>"#{v}/1 days" } }
#=> [{"Country"=>"Brazil", "Duration"=>"971/1 days"},
# {"Country"=>"Argentina", "Duration"=>"383/1 days"}]
The first hash passed to the block and assigned to the block variable g.
g = {"Country"=>"Brazil", "Duration"=>"731/1 days"}
At this time h #=> {}. We then compute
h[g["Country"]] += g["Duration"].to_i
#=> h["Brazil"] += "971/1 days".to_i
#=> h["Brazil"] = h["Brazil"] + 971
#=> h["Brazil"] = 0 + 971 # h["Brazil"]
See String#to_i for an explanation of why "971/1 days".to_i returns 971.
h["Brazil"] on the right of the equality returns the default value of 0 because h does not (yet) have a key "Brazil". Note that h["Brazil"] on the right is syntactic sugar for h.[]("Brazil"), whereas on the left it is syntactic sugar for h.[]=(h["Brazil"] + 97). It is Hash#[] that returns the default value when the hash does not have the given key. The remaining steps are similar.

You may update your code as follows:
countries.inject do |new_h, old_h|
new_h.merge(old_h) do |k, old_v, new_v|
if k=="Country" then old_v else old_v + new_v end
end
end
# => {"Country"=>"Brazil", "Duration"=>1096}
where you basically use the k (for key) argument to switch among different merging policies.

Related

Ruby creating a new hash from an array of key, value

first_response = [
{"xId" => "123", "yId" => "321"},
{"xId" => "x", "yId" => "y" }
]
first_response.each do |resp|
x_id = resp['xId']
y_id = resp['yId']
puts x_id.to_s
puts y_id.to_s
end
This gives me outputs
123
321
x
y
output hash I want to create is
{123=>{321}, x=>{y}}
first service: I have an array of hash that has two different ids example:(x_id and y_id) (there would be multiple pairs like that in the response)
I want to create a hash that should contain the matching pair of x_id and y_ids that we get from the first service with x_id's as the key for all the pairs.
If you know every hash in first_response is going to contain exactly two key/value pairs, you can extract their values and then convert that result into a hash (see Enumerable#to_h):
first_response.to_h(&:values)
# {"123"=>"321", "x"=>"y"}
Looks like this approach works, but I am not completely sure if that is right
first_response = [{"xId"=>"123","yId"=> "321"}, {"xId"=>"x","yId"=> "y"}]
h = {}.tap do |element|
first_response.each do |resp|
x_id = resp['xId']
y_id = resp['yId']
element[x_id] = y_id
end
end
puts h.to_s
# {"123"=>"321", "x"=>"y"}

Match data set against the predefined data-set and store it in a hash format

I need to be able to match the data from an array of random ints and pass them to a hash in a specific outline while matching the random data against a defined set of data.
empty = {}
defined = [1,2,17,34,3,18,17]
dataset_one = [1,0,17]
dataset_two = [3,18,2,4]
desired = { 1 => 3, 17 => 2}
This is what I have so far:
defined.each{ |item|
dataset_one.each{ |key|
if item == key
empty[key] = nil
end
dataset_two.each{ |value|
if item = value
empty[key] = value
}
}
}
Pair up the keys and the values, eliminate those where the key is not in the predetermined set of data, then turn the list of key-value pairs into a hash.
dataset_one.zip(dataset_two).select { |k, v| defined.include?(k) }.to_h
# => {1=>3, 17=>2}
It is not clear from the question if you need to check both keys and values against defined; if so, the modification is trivial:
dataset_one.zip(dataset_two).select { |k, v|
defined.include?(k) && defined.include?(v)
}.to_h
If speed is important, you might want to turn some of your stuff into sets (defined in particular).

How to merge 2 strings alternately in rails?

I have 2 strings:
a = "qwer"
b = "asd"
Result = "qawsedr"
Same is the length of b is greater than a. show alternate the characters.
What is the best way to do this? Should I use loop?
You can get the chars from your a and b string to work with them as arrays and then "merge" them using zip, then join them.
In the case of strings with different length, the array values must be reversed, so:
def merge_alternately(a, b)
a = a.chars
b = b.chars
if a.length >= b.length
a.zip(b)
else
array = b.zip(a)
array.map{|e| e != array[-1] ? e.reverse : e}
end
end
p merge_alternately('abc', 'def').join
# => "adbecf"
p merge_alternately('ab', 'zsd').join
# => "azbsd"
p merge_alternately('qwer', 'asd').join
# => "qawsedr"
Sebastián's answer gets the job done, but it's needlessly complex. Here's an alternative:
def merge_alternately(a, b)
len = [a.size, b.size].max
Array.new(len) {|n| [ a[n], b[n] ] }.join
end
merge_alternately("ab", "zsd")
# => "azbsd"
The first line gets the size of the longer string. The second line uses the block form of the Array constructor; it yields the indexes from 0 to len-1 to the block, resulting in an array like [["a", "z"], ["b", "s"], [nil, "d"]]. join turns it into a string, conveniently calling to_s on each item, which turns nil into "".
Here's another version that does basically the same thing, but skips the intermediate arrays:
def merge_alternately(a, b)
len = [a.size, b.size].max
len.times.reduce("") {|s, i| s + a[i].to_s + b[i].to_s }
end
len.times yields an Enumerator that yields the indexes from 0 to len-1. reduce starts with an empty string s and in each iteration appends the next characters from a and b (or ""—nil.to_s—if a string runs out of characters).
You can see both on repl.it: https://repl.it/I6c8/1
Just for fun, here's a couple more solutions. This one works a lot like Sebastián's solution, but pads the first array of characters with nils if it's shorter than the second:
def merge_alternately(a, b)
a, b = a.chars, b.chars
a[b.size - 1] = nil if a.size < b.size
a.zip(b).join
end
And it wouldn't be a Ruby answer without a little gsub:
def merge_alternately2(a, b)
if a.size < b.size
b.gsub(/./) { a[$`.size].to_s + $& }
else
a.gsub(/./) { $& + b[$`.size].to_s }
end
end
See these two on repl.it: https://repl.it/I6c8/2

How to group and count the last N results?

In Rails, you can use:
Model.group(:field).count
to yield something like:
{"a"=>7, "b"=>5, "c"=>3 "d"=>3, "e"=>4}
But how can I count ONLY in the last N lines, not the entire table, with the DATABASE doing the calculations?
Do not work:
Model.limit(100).group(:field).count
limit will limit the hash output keys not the table lines used
Model.last(100).group(:field).count
Last returns a Array and raises an error
I'm using:
* Ruby 2.3.3p222
* Rails 4.2.4
* pg 9.5.6
As you mentioned, the limit is being applied on the grouped instances, not the instances themselves. A simple workaround would be:
Model.where(id: Model.limit(100).select(:id)).group(:field).count
Array objects can also be grouped using group_by:
grouped = Model.last(100).group_by(&:field).map { |k,v| [k, v.length] }
This will return the following matrix:
#=> [["Field value 1", value_1_count], ["Field value 2", value_2_count], etc...]
Matrix can also be turned into hash:
grouped.each_with_object({}) { |value, memo| memo[value[0]] = value[1] }
To sum up, try the following:
Model.last(100)
.group_by(&:field)
.each_with_object({}) { |(key, value), memo| memo[key] = value.length }

There has got to be a cleaner way to do this

I have this code here and it works but there has to be a better way.....i need two arrays that look like this
[
{
"Vector Arena - Auckland Central, New Zealand" => {
"2010-10-10" => [
"Enter Sandman",
"Unforgiven",
"And justice for all"
]
}
},
{
"Brisbane Entertainment Centre - Brisbane Qld, Austr..." => {
"2010-10-11" => [
"Enter Sandman"
]
}
}
]
one for the past and one for the upcoming...the problem i have is i am repeating myself and though it works i want to clean it up ...here is my data
..
Try this:
h = Hash.new {|h1, k1| h1[k1] = Hash.new{|h2, k2| h2[k2] = []}}
result, today = [ h, h.dup], Date.today
Request.find_all_by_artist("Metallica",
:select => "DISTINCT venue, showdate, LOWER(song) AS song"
).each do |req|
idx = req.showdate < today ? 0 : 1
result[idx][req.venue][req.showdate] << req.song.titlecase
end
Note 1
In the first line I am initializing an hash of hashes. The outer hash creates the inner hash when a non existent key is accessed. An excerpt from Ruby Hash documentation:
If this hash is subsequently accessed by a key that doesn‘t correspond to a hash
entry, the block will be called with the hash object and the key, and should
return the default value. It is the block‘s responsibility to store the value in
the hash if required.
The inner hash creates and empty array when the non existent date is accessed.
E.g: Construct an hash containing of content as values and date as keys:
Without a default block:
h = {}
list.each do |data|
h[data.date] = [] unless h[data.date]
h[data.date] << data.content
end
With a default block
h = Hash.new{|h, k| h[k] = []}
list.each do |data|
h[data.date] << data.content
end
Second line simply creates an array with two items to hold the past and future data. Since both past and the present stores the data as Hash of Hash of Array, I simply duplicate the value.
Second line can also be written as
result = [ h, h.dup]
today = Date.today

Resources