How to find the most frequent value in array of hashes - ruby

I have the array of objects "orders". I want to obtain a first most frequent value in the my array, who often takes a book:
orders = [
{'book' => '1', 'reader' => 'Denis' },
{'book' => '2', 'reader' => 'Mike' },
{'book' => '3', 'reader' => 'Denis' },
{'book' => '3', 'reader' => 'Mike' },
{'book' => '5', 'reader' => '2' }
]
I tried this method, but it's good only for arrays of strings: ['string', 'string'...]:
def most_common_value(a)
a.group_by(&:itself).values.max_by(&:size).first
end
Expected result:
=> "Denis"

I'd do it like this:
orders = [
{'book' => '1', 'reader' => 'Denis' },
{'book' => '2', 'reader' => 'Mike' },
{'book' => '3', 'reader' => 'Denis' },
{'book' => '3', 'reader' => 'Mike' },
{'book' => '5', 'reader' => '2' }
]
orders.each_with_object(Hash.new(0)) { |order, hash| hash[order['reader']] += 1 }.max_by { |k, v| v }
# => ["Denis", 2]
The problem with this is, if there are multiple "max" then the result will be returned based on the order the data is found. For instance, if the order is different:
orders.push(orders.shift)
# => [{"book"=>"2", "reader"=>"Mike"},
# {"book"=>"3", "reader"=>"Denis"},
# {"book"=>"3", "reader"=>"Mike"},
# {"book"=>"5", "reader"=>"2"},
# {"book"=>"1", "reader"=>"Denis"}]
the result changes:
orders.each_with_object(Hash.new(0)) { |order, hash| hash[order['reader']] += 1 }.max_by { |k, v| v }
# => ["Mike", 2]

key = 'reader'
x = orders.inject({}) do |a,i|
a[i[key]] = 0 unless a.has_key? i[key]
a[i[key]] +=1
a
end.max_by{|k,v| v}
Which returns:
=> ["Denis", 2]

I saw this a few days ago and thought this would be easy if Array or Enumerable had a mode_by! Well I finally got around to whipping one up.
Implementing mode_by
A true mode_by would probably return a subarray of the items matching a block:
orders.mode_by{|order| order['reader']}
#=> [{'book'=>'1', 'reader'=>'Denis'}, {'book'=>'3', 'reader'=>'Denis'}]
and to get your desired result:
orders.mode_by{|order| order['reader']}.first['reader']
#=> "Denis"
So let's implement a mode_by:
class Array
def mode_by(&block)
self.group_by(&block).values.max_by(&:size)
end
end
et voila!
A custom alternative
In your case, you don't need to return array elements. Let's simplify further by implementing something that returns exactly what you want, the first result of the block that appears the most. We'll call it mode_of:
class Array
def mode_of(&block)
self.group_by(&block).max_by{|k,v| v.size}.first
end
end
Now you can simply do this:
orders.mode_of{|order| order['reader']}
#=> "Denis"

using group_by and max_by :
orders.group_by { |h| h['reader']}.to_a.max_by {|x| x[1].length}.first
Output :
=> "Denis"

Related

How can I do a ruby method that converts morse code into numbers?

I made this ruby method to convert numbers to morse code and it works, but now I'm trying to do the reverse (convert a morse code input into numbers), and I can't figure out how.
def convert(morse_code)
morse_code = {
"1" => ".----",
"2" => "..---",
"3" => "...--",
"4" => "....-",
"5" => ".....",
"6" => "-....",
"7" => "--...",
"8" => "---..",
"9" => "----.",
"0" => "-----"
}
#converted = gets.chomp().downcase.gsub(/\w/, morse_code)
end
puts convert(#converted)
I tried to change the places of the strings in the hash but it actually won't work.
What I've tried so far:
def convert(morse_code)
morse_code = {
'.----': '1', '..---': '2',
'...--': '3', '....-': '4',
'.....': '5', '-....': '6',
'--...': '7', '---..': '8',
'----.': '9', '-----': '0'
}
#converted = gets.chomp().gsub(/\w/, morse_code)
end
puts convert(#converted)
You're redefining the parameter morse_code with your hash, so your param is useless.
use => to have strings as key in hash, like { 'key' => 'value' }
At the end, it looks better like this:
MORSE_CODE_TO_NUMBERS = {
'.----' => 1, '..---' => 2,
'...--' => 3, '....-' => 4,
'.....' => 5, '-....' => 6,
'--...' => 7, '---..' => 8,
'----.' => 9, '-----' => 0
}
def convert_numbers_to_morse
puts 'Enter your morse phrase:'
msg = gets
word_array = msg.split
word_array_converted = word_array.map{|code| MORSE_CODE_TO_NUMBERS[code]}
word_array_converted.join(' ')
end
puts convert_numbers_to_morse

Getting data from an array of hashes in ruby

I get an array of hashes from a google sheet, it looks like this
itemList = [ {:id => '1', :type => 'A', :category => 'Cat1' },
{:id => '2', :type => 'A', :category => 'Cat1' },
{:id => '3', :type => 'B', :category => 'Cat1' },
{:id => '4', :type => 'B', :category => 'Cat1' },
{:id => '1', :type => 'A', :category => 'Cat2' },
{:id => '2', :type => 'A', :category => 'Cat2' },
{:id => '3', :type => 'C', :category => 'Cat2' } ]
I would like to be able to print this on the terminal
Cat1
A
1, 2
B
3, 4
Cat2
A
1, 2
C
3
Is there an easy way to do it?
Thank you
Following will provide you required format,
items = itemList.group_by {|x| x[:category] }
val = items.inject({}) do |m,(k,v)|
tmp = v.group_by { |x| x[:type] }
m[k] = tmp.update(tmp) { |i,j| j.map { |x| x[:id] } }
m
end
# => {"Cat1"=>{"A"=>["1", "2"], "B"=>["3", "4"]}, "Cat2"=>{"A"=>["1", "2"], "C"=>["3"]}}
Display it like below,
val.each { |k,v| puts k; v.each { |i,j| puts i; puts j.join(', ') }; puts }
Cat1
A
1, 2
B
3, 4
Cat2
A
1, 2
C
3

Most performant way to group/summarise two hashes?

I have two hashes with some data that I need to aggregate. The first one is a mapping of which ids (id_1, id_2, id_3, id_4) belong under what category (a, b, c):
hash_1 = {'a' => ['id_1','id_2'], 'b' => ['id_3'], 'c' => ['id_4']}
The second hash holds values of how many events happened per id for a given date (date_1, date_2, date_3):
hash_2 = {
'id_1' => {'date_1' => 5, 'date_2' => 6, 'date_3' => 8},
'id_2' => {'date_1' => 0, 'date_3' => 6},
'id_3' => {'date_1' => 0, 'date_2' => nil, 'date_3' => 1},
'id_4' => {'date_1' => 10, 'date_2' => 1}
}
What I want is to get the total event per category (a,b,c). For the above example, the result would look something like:
hash_3 = {'a' => (5+6+8+0+6), 'b' => (0+0+1), 'c' => (10+1)}
My problem is, that there are about 5000 categories, each pointing to typically 1 to 3 ids, and each ID having event counts for 30 dates or more. So this takes quite a bit of computation. What will be the most performant (time effective) way to do this grouping in Ruby?
update
This is what I tried so far (took like 6-8 seconds!, horribly slow):
def total_clicks_per_category
{}.tap do |res|
hash_1.each do |cat, ids|
res[cat] = total_event_per_ids(ids)
end
end
end
def total_event_per_ids(ids)
ids.reduce(0) do |memo, id|
events = hash_2.fetch(id, {})
memo + (events.values.reduce(:+) || 0)
end
end
P.S. I’m using Ruby 2.3.
I'm writing this on a phone so I cannot test right now, but it looks OK.
g = hash_2.each_with_object({}) { |(k,v),g| g[k] = v.values.compact.sum }
hash_3 = hash_1.each_with_object({}) { |(k,v),h| h[k] = g.values_at(*v).sum }
First, create an intermediate hash that holds the sum of hash_2:
hash_4 = hash_2.map{|k, v| [k, v.values.inject(:+)]}.to_h
# => {"id_1"=>19, "id_2"=>6, "id_3"=>1, "id_4"=>11}
Then do the final summation:
hash_3 = hash_1.map{|k, v| [k, v.map{|k| hash_4[k]}.inject(:+)]}.to_h
# => {"a"=>25, "b"=>1, "c"=>11}
Theory
5000*3*30 isn't that many. Ruby probably will need a second at most for this kind of job.
Hash lookup is fast by default, you won't be able to optimize much.
You could pre-calculate hash_2_sum, though :
hash_2_sum = {
'id_1' => 5+6+8,
'id_2' => 0+6,
'id_3' => 0+0+1,
'id_4' => 10+1
}
A loop on hash1 with hash_2_sum lookup, and you're done.
Code
Your example has been updated with some nil values. You need to remove them with compact, and make sure the sum is 0 when no element is found with inject(0, :+):
hash_1 = {'a' => ['id_1','id_2'], 'b' => ['id_3'], 'c' => ['id_4']}
hash_2 = {
'id_1' => { 'date_1' => 5, 'date_2' => 6, 'date_3' => 8 },
'id_2' => { 'date_1' => 0, 'date_3' => 6 },
'id_3' => { 'date_1' => 0, 'date_2' => nil, 'date_3' => 1 },
'id_4' => { 'date_1' => 10, 'date_2' => 1 }
}
hash_2_sum = hash_2.each_with_object({}) do |(key, dates), sum|
sum[key] = dates.values.compact.inject(0, :+)
end
hash_3 = hash_1.each_with_object({}) do |(key, ids), sum|
sum[key] = hash_2_sum.values_at(*ids).inject(0, :+)
end
# {"a"=>25, "b"=>1, "c"=>11}
Note
{}.tap do |res|
hash_1.each do |cat, ids|
res[cat] = total_event_per_ids(ids)
end
end
isn't very readable IMHO.
You can either use each_with_object or Array#to_h :
result = [1, 2, 3].each_with_object({}) do |i, hash|
hash[i] = i * i
end
#=> {1=>1, 2=>4, 3=>9}
result = [1, 2, 3].map { |i| [i, i * i] }.to_h
#=> {1=>1, 2=>4, 3=>9}

Merge hashes based on particular key/value pair in ruby

I am trying to merge an array of hashes based on a particular key/value pair.
array = [ {:id => '1', :value => '2'}, {:id => '1', :value => '5'} ]
I would want the output to be
{:id => '1', :value => '7'}
As patru stated, in sql terms this would be equivalent to:
SELECT SUM(value) FROM Hashes GROUP BY id
In other words, I have an array of hashes that contains records. I would like to obtain the sum of a particular field, but the sum would grouped by key/value pairs. In other words, if my selection criteria is :id as in the example above, then it would seperate the hashes into groups where the id was the same and the sum the other keys.
I apologize for any confusion due to the typo earlier.
Edit: The question has been clarified since I first posted my answer. As a result, I have revised my answer substantially.
Here are two "standard" ways of addressing this problem. Both use Enumerable#select to first extract the elements from the array (hashes) that contain the given key/value pair.
#1
The first method uses Hash#merge! to sequentially merge each array element (hashes) into a hash that is initially empty.
Code
def doit(arr, target_key, target_value)
qualified = arr.select {|h|h.key?(target_key) && h[target_key]==target_value}
return nil if qualified.empty?
qualified.each_with_object({}) {|h,g|
g.merge!(h) {|k,gv,hv| k == target_key ? gv : (gv.to_i + hv.to_i).to_s}}
end
Example
arr = [{:id => '1', :value => '2'}, {:id => '2', :value => '3'},
{:id => '1', :chips => '4'}, {:zd => '1', :value => '8'},
{:cat => '2', :value => '3'}, {:id => '1', :value => '5'}]
doit(arr, :id, '1')
#=> {:id=>"1", :value=>"7", :chips=>"4"}
Explanation
The key here is to use the version of Hash#merge! that uses a block to determine the value for each key/value pair whose key appears in both of the hashes being merged. The two values for that key are represented above by the block variables hv and gv. We simply want to add them together. Note that g is the (initially empty) hash object created by each_with_object, and returned by doit.
target_key = :id
target_value = '1'
qualified = arr.select {|h|h.key?(target_key) && h[target_key]==target_value}
#=> [{:id=>"1", :value=>"2"},{:id=>"1", :chips=>"4"},{:id=>"1", :value=>"5"}]
qualified.empty?
#=> false
qualified.each_with_object({}) {|h,g|
g.merge!(h) {|k,gv,hv| k == target_key ? gv : (gv.to_i + hv.to_i).to_s}}
#=> {:id=>"1", :value=>"7", :chips=>"4"}
#2
The other common way to do this kind of calculation is to use Enumerable#flat_map, followed by Enumerable#group_by.
Code
def doit(arr, target_key, target_value)
qualified = arr.select {|h|h.key?(target_key) && h[target_key]==target_value}
return nil if qualified.empty?
qualified.flat_map(&:to_a)
.group_by(&:first)
.values.map { |a| a.first.first == target_key ? a.first :
[a.first.first, a.reduce(0) {|tot,s| tot + s.last}]}.to_h
end
Explanation
This may look complex, but it's not so bad if you break it down into steps. Here's what's happening. (The calculation of qualified is the same as in #1.)
target_key = :id
target_value = '1'
c = qualified.flat_map(&:to_a)
#=> [[:id,"1"],[:value,"2"],[:id,"1"],[:chips,"4"],[:id,"1"],[:value,"5"]]
d = c.group_by(&:first)
#=> {:id=>[[:id, "1"], [:id, "1"], [:id, "1"]],
# :value=>[[:value, "2"], [:value, "5"]],
# :chips=>[[:chips, "4"]]}
e = d.values
#=> [[[:id, "1"], [:id, "1"], [:id, "1"]],
# [[:value, "2"], [:value, "5"]],
# [[:chips, "4"]]]
f = e.map { |a| a.first.first == target_key ? a.first :
[a.first.first, a.reduce(0) {|tot,s| tot + s.last}] }
#=> [[:id, "1"], [:value, "7"], [:chips, "4"]]
f.to_h => {:id=>"1", :value=>"7", :chips=>"4"}
#=> {:id=>"1", :value=>"7", :chips=>"4"}
Comment
You may wish to consider makin the values in the hashes integers and exclude the target_key/target_value pairs from qualified:
arr = [{:id => 1, :value => 2}, {:id => 2, :value => 3},
{:id => 1, :chips => 4}, {:zd => 1, :value => 8},
{:cat => 2, :value => 3}, {:id => 1, :value => 5}]
target_key = :id
target_value = 1
qualified = arr.select { |h| h.key?(target_key) && h[target_key]==target_value}
.each { |h| h.delete(target_key) }
#=> [{:value=>2}, {:chips=>4}, {:value=>5}]
return nil if qualified.empty?
Then either
qualified.each_with_object({}) {|h,g| g.merge!(h) { |k,gv,hv| gv + hv } }
#=> {:value=>7, :chips=>4}
or
qualified.flat_map(&:to_a)
.group_by(&:first)
.values
.map { |a| [a.first.first, a.reduce(0) {|tot,s| tot + s.last}] }.to_h
#=> {:value=>7, :chips=>4}

Ruby loops. Hash into string

I am working with Ruby. I need to grab each key/value and put it into a string.
So far I have:
values = ['first' => '1', 'second' => '2']
#thelink = values.collect do | key, value |
"#{key}=#{value}&"
end
When I print #thelink I see:
first1second2=&
But Really what I want is
first=1&second=2
Could anybody help/explain please?
There is something subtle you are missing here {} vs [].
See the below taken from IRB tests:
irb(main):002:0> {'first' => 1, 'second' => 2}
=> {"second"=>2, "first"=>1}
irb(main):003:0> ['first' => 1, 'second' => 2]
=> [{"second"=>2, "first"=>1}]
irb(main):004:0> {'first' => 1, 'second' => 2}.class
=> Hash
irb(main):005:0> ['first' => 1, 'second' => 2].class
=> Array
Similar to this:
irb(main):006:0> {'first' => 1, 'second' => 2}.collect { |key,value| puts "#{key}:#{value}" }
second:2
first:1
=> [nil, nil]
irb(main):007:0> ['first' => 1, 'second' => 2].collect { |key,value| puts "#{key}:#{value}" }
second2first1:
=> [nil]
The array has a single element (a hash) that, as a string, is everything concatenated. This is the important thing to note here.
On the other hand, the hash iterates by handing you the key/value pairs that you are expecting.
Hope that helps.
I think your code has a typo (a hash is delimited by {} not by []). Try this
values = {'first' => '1', 'second' => '2'}
r = values.map{|k,v| "#{k}=#{v}"}.join('&')
puts r
#shows: first=1&second=2

Resources