Iterating over hash of arrays - ruby

I have the following:
#products = {
2 => [
#<Review id: 9, answer01: 3, score: 67, style_id: 2, consumer_id: 2,
branch_id: 2, business_id: 2>
],
15 => [
#<Review id: 10, answer01: 3, score: 67, style_id: 2, consumer_id: 2,
branch_id: 2, business_id: 2>,
#<Review id: 11, answer01: 3, score: 67, style_id: 2, consumer_id: 2,
branch_id: 2, business_id: 2>
]
}
I want to average the scores for all reviews associated with each product's hash key. How can I do this?

To iterate over a hash:
hash = {}
hash.each_pair do |key,value|
#code
end
To iterate over an array:
arr=[]
arr.each do |x|
#code
end
So iterating over a hash of arrays (let's say we're iterating over each array in each point in the hash) would be done like so:
hash = {}
hash.each_pair do |key,val|
hash[key].each do |x|
#your code, for example adding into count and total inside program scope
end
end

Yes, just use map to make and array of the scores for each product and then take the average of the array.
average_scores = {}
#products.each_pair do |key, product|
scores = product.map{ |p| p.score }
sum = scores.inject(:+) # If you are using rails, you can also use scores.sum
average = sum.to_f / scores.size
average_scores[key] = average
end

Thanks for the answer Shingetsu, I will certainly upvote it. I accidentally figured the answer out myself.
trimmed_hash = #products.sort.map{|k, v| [k, v.map{|a| a.score}]}
trimmed_hash.map{|k, v| [k, v.inject(:+).to_f/v.length]}

Related

Merging/adding hashes in array with same key

I have an array of hashes:
array = [
{"points": 0, "block": 3},
{"points": 25, "block": 8},
{"points": 65, "block": 4}
]
I need to merge the hashes. I need the output to be:
{"points": 90, "block": 15}
You could merge the hashes together, adding the values that are in both hashes:
result = array.reduce do |memo, next_hash|
memo.merge(next_hash) do |key, memo_value, next_hash_value|
memo_value + next_hash_value
end
end
result # => {:points=>90, :block=>15}
and if your real hash has keys that don't respond well to +, you have access to the key, you could set up a case statement, to handle the keys differently, if needed.
If you have the array as you mentioned in this structure:
array = [
{"points": 0, "block": 3},
{"points": 25, "block": 8},
{"points": 65, "block": 4}
]
You can use the following code to achieve your goal:
result = {
points: array.map{ |item| item[:points] }.inject(:+),
block: array.map{ |item| item[:block] }.inject(:+)
}
You will get this result:
{:points=>90, :block=>15}
Note: This will iterate twice over the array. I'm trying to figure out a better way to iterate once and still have the same elegant/easy to ready code.
If you want to do it more generically (more keys than :points and :block), then you can use this code:
array = [
{"points": 0, "block": 3},
{"points": 25, "block": 8},
{"points": 65, "block": 4}
]
keys = [:points, :block] # or you can make it generic with array.first.keys
result = keys.map do |key|
[key, array.map{ |item| item.fetch(key, 0) }.inject(:+)]
end.to_h
You can create method as below to get the result
def process_array(array)
points = array.map{|h| h[:points]}
block = array.map{|h| h[:block]}
result = {}
result['points'] = points.inject{ |sum, x| sum + x }
result['block'] = block.inject{ |sum, x| sum + x }
result
end
and calling the method with array input will give you expected result.
[54] pry(main)> process_array(array)
=> {"points"=>90, "block"=>15}
You can also use the Enumerator each_with_object, using a hash as object.
result = array.each_with_object(Hash.new(0)) {|e, h| h[:points] += e[:points]; h[:block] += e[:block] }
# => {:points=>90, :block=>15}
Hash.new(0) means initialise the hash to default value 0 for any keys, for example:
h = Hash.new(0)
h[:whathever_key] # => 0
I was interested in how the reduce method introduced by "Simple Lime" worked and also how it would benchmark against simple iteration over the array and over the keys of each hash.
Here is the code of the "iteration" approach:
Hash.new(0).tap do |result|
array.each do |hash|
hash.each do |key, val|
result[key] = result[key] + val
end
end
end
I was surprised, that the "iteration" code performed 3 times better than the reduce approach.
Here is the benchmark code https://gist.github.com/landovsky/6a1b29cbf13d0cf81bad12b6ba472416

How to convert a three-line Ruby method into one

I have a simple method that iterates through an array and returns a duplicate. (Or duplicates)
def find_dup(array)
duplicate = 0
array.each { |element| duplicate = element if array.count(element) > 1}
duplicate
end
It works, but I'd like to express this more elegantly.
The reason it is three lines is that the variable "duplicate", which the method must return, is not visible to the method if I introduce it inside the block, i.e,
def find_dup(array)
array.each { |element| duplicate = element if array.count(element) > 1}
duplicate
end
I've tried a few ways to define "duplicate" as the result of a block, but to no avail.
Any thoughts?
It's a little too much to do cleanly in a one-liner, but this is a more
efficient solution.
def find_dups(arr)
counts = Hash.new { |hash,key| hash[key] = 0 }
arr.each_with_object(counts) do |x, memo|
memo[x] += 1
end.select { |key,val| val > 1 }.keys
end
The Hash.new call instantiates a hash where the default value is 0.
each_with_object modifies this hash to track the count of each element in arr, then at the
end the filter is used to select only those having a count greater than one.
The benefit of this approach over a solution using Array#includes? or Array#count is that it only scans the array a single time. Thus it is a O(N) time instead of O(N^2).
Your method is only finding the last duplicate in the array. If you want all the duplicates, I would do something like this:
def find_dups(arr)
dups = Hash.new { |h, k| h[k] = 0 }
arr.each { |el| dups[el] += 1 }
dups.select { |k, v| v > 1 }.keys
end
If what you really want is a one-liner that isn't concerned with big-O complexity and only returns the last duplicate in the array, I would do this:
def find_last_dup(arr)
arr.reverse_each { |el| return el if arr.count(el) > 1 }
end
You can do this as one line and it flows a bit nicer. Though this would find the first instance of a duplicate whereas your code is returning the last instance of a duplicate, not sure if that's part of your requirement.
def find_dup(array)
array.group_by { |value| value }.find { |_, groups| groups.count > 1 }.first
end
Also, note that making things one line doesn't strictly mean is better. I'd find the code more readable split over more lines, but that's just my opinion.
def find_dup(array)
array.group_by { |value|
value
}.find { |_, groups|
groups.count > 1
}.first
end
Just want to add one more approach to the mix.
def find_last_dup(arr)
arr.reverse_each.detect { |x| arr.count(x) > 1 }
end
Alternatively, you can get linear time complexity in two lines.
def find_last_dup(arr)
freq = arr.each_with_object(Hash.new(0)) { |x, obj| obj[x] += 1 }
arr.reverse_each.detect { |x| freq[x] > 1 }
end
For the sake of argument, the latter approach can be reduced to one line as well, but this would be unidiomatic and confusing.
def find_last_dup(arr)
arr.each_with_object(Hash.new(0)) { |x, obj| obj[x] += 1 }
.tap do |freq| return arr.reverse_each.detect { |x| freq[x] > 1 } end
end
Given:
> a
=> [8, 5, 6, 6, 5, 8, 6, 1, 9, 7, 2, 10, 7, 7, 3, 4]
You can group the dups together:
> a.uniq.each_with_object(Hash.new(0)) {|e, h| c=a.count(e); h[e]=c if c>1}
=> {8=>2, 5=>2, 6=>3, 7=>3}
Or,
> a.group_by{ |e| e}.select{|k,v| v if v.length>1}
=> {8=>[8, 8], 5=>[5, 5], 6=>[6, 6, 6], 7=>[7, 7, 7]}
In each case, the order of the result is based on the order of the elements in a that have dups. If you just want the first:
> a.group_by{ |e| e}.select{|k,v| v if v.length>1}.first
=> [8, [8, 8]]
Or last:
> a.group_by{ |e| e}.select{|k,v| v if v.length>1}.to_a.last
=> [7, [7, 7, 7]]
If you want to 'fast forward' to the first value that has a dup, you can use drop_while:
> b=[1,2,3,4,5,4,5,6]
> b.drop_while {|e| b.count(e)==1 }[0]
=> 4
Or the last:
> b.reverse.drop_while {|e| b.count(e)==1 }[0]
=> 5
def find_duplicates(array)
array.dup.uniq.each { |element| array.delete_at(array.index(element)) }.uniq
end
The above method find_duplicates duplicated the input array and deletes the first occurrence of all the elements, leaving the array with only remaining occurrences of the duplicate elements.
Example:
array = [1, 2, 3, 4, 3, 4, 3]
=> [1, 2, 3, 4, 3, 4, 3]
find_duplicates(array)
=> [3, 4]

Ruby Array default value?

I have hundreds of arrays that am normalizing for a CSV.
[
["foo", "tom", nil, 1, 4, "cheese"],
["foo", "tom", "fluffy",nil, 4],
["foo", "tom", "fluffy",1, nil],
...
]
Currently to make them all equal length i am finding the max length and setting to a value.
rows.each { |row| row[max_index] ||= nil }
this is cool because it makes the array length equal to the new length.
Instead of appending a bunch of nils at the end I needed to append COLUMN_N where N is the index (1-based).
table_rows.each do |row|
last_index = row.length - 1
(last_index..max_index).to_a.each { |index| row[index] ||= "COLUMN_#{index+1}" }
end
Which seemed like an awkward way to have a default value that is a function of the index.
You can't choose a default value for filling elements with []= method. But you can easily do something like this if there aren't other nils that you don't want to replace.
row.each_with_index.map { |item, index| item.nil? ? "column_#{index}": item }
To get a default value instead of nil you can use fetch:
row = ["foo", "tom", "fluffy", 1, 4]
row.fetch(7) { |i| "COLUMN_#{i + 1}" }
=> "COLUMN_8"
But it won't fill the array for you.
Also see: Can I create an array in Ruby with default values?
This seems like it could work for you.
class Array
def push_with_default(item, index, &block)
new_arr = Array.new([self.size + 1, index].max, &block)
self[index] = item
self.map!.with_index { |n, i| n.nil? ? new_arr[i] : n }
end
end
>> array = [1,2,5,9]
[
[0] 1,
[1] 2,
[2] 5,
[3] 9
]
>> array.push_with_default(2, 10) { |i| "column_#{i}" }
[
[ 0] 1,
[ 1] 2,
[ 2] 5,
[ 3] 9,
[ 4] "column_4",
[ 5] "column_5",
[ 6] "column_6",
[ 7] "column_7",
[ 8] "column_8",
[ 9] "column_9",
[10] 2
]
I don't believe a method like this exists on Array already though.

Grouping an array on the basis of its first element, without duplication in Ruby

I'm executing an active record command Product.pluck(:category_id, :price), which returns an array of 2 element arrays:
[
[1, 500],
[1, 100],
[2, 300]
]
I want to group on the basis of the first element, creating a hash that looks like:
{1 => [500, 100], 2 => [300]}
group_by seems logical, but replicates the entire array. I.e. a.group_by(&:first) produces:
{1=>[[1, 500], [1, 100]], 2=>[[2, 300]]}
You can do a secondary transform to it:
Hash[
array.group_by(&:first).collect do |key, values|
[ key, values.collect { |v| v[1] } ]
end
]
Alternatively just map out the logic directly:
array.each_with_object({ }) do |item, result|
(result[item[0]] ||= [ ]) << item[1]
end
This one-liner seemed to work for me.
array.group_by(&:first).map { |k, v| [k, v.each(&:shift)] }.to_h
Since you're grouping by the first element, just remove it with shift and turn the result into a hash:
array.group_by(&:first).map do |key, value|
value = value.flat_map { |x| x.shift; x }
[key, value]
end #=> {1=>[500, 100], 2=>[300]}
I do not like the destructive operation.
array.group_by(&:first).map { |id, a| [id, a.map(&:last)] }.to_h
Used this functionality several times in my app, added extension to an array:
# config/initializers/array_ext.rb
class Array
# given an array of two-element arrays groups second element by first element, eg:
# [[1, 2], [1, 3], [2, 4]].group_second_by_first #=> {1 => [2, 3], 2 => [4]}
def group_second_by_first
each_with_object({}) { |(first, second), h| (h[first] ||= []) << second }
end
end

How to find number of a specific integer in an array of Fixnums ruby

I'm doing an exercise now where I'm looking for all of the zeros in an array.
The input is:
numbers = [1, 3, 500, 200, 4000, 3000, 10000, 90, 20, 500000]
I want to sort them into a hash by the number of zeros. The expected output is:
expected = {0=>[1, 3], 2=>[500, 200], 3=>[4000, 3000], 4=>[10000], 1=>[90, 20], 5=>[500000]}
I have the structure built but I'm not sure how to count the number of zeros:
grouped = Hash.new {|hash, key| hash[key] = []}
numbers.each do |num|
grouped[num] << num
end
EDITED for clarity:
Any advice would be appreciated. Also, a lot of the advice I read on this recommended converting the array of integers to a string in order to solve the problem. Is there a way to count the number of digits (not just zeros) without converting the array to a string? The expected output in this case would look like:
expected = {1=>[1, 3], 2=>[90, 20], 3=>[500, 200], 4=>[4000, 3000], 5=>[10000], 6=>[500000]
Thanks in advance.
Like many transformations you'll want to do, this one's found in Enumerable.
Grouping by number of digits:
grouped = numbers.group_by { |n| Math.log10(n).to_i + 1 }
# => {1=>[1, 3], 3=>[500, 200], 4=>[4000, 3000], 5=>[10000], 2=>[90, 20], 6=>[500000]}
Grouping by number of zeroes:
grouped = numbers.group_by { |n| n.to_s.match(/0+$/) ? $&.length : 0 }
# => {0=>[1, 3], 2=>[500, 200], 3=>[4000, 3000], 4=>[10000], 1=>[90, 20], 5=>[500000]}
The group_by method is a handy way to convert an Array to a Hash with things organized into pigeon-holes.
I wound up using
grouped = Hash.new {|hash, key| hash[key] = []}
numbers.each do |num|
grouped[num.to_s.count('0')] << num
end
but I really liked the variation in responses. I didn't realize there were so many ways to go about this. Thank you everyone.
If you wish to group non-negative integers by the number of zero digits they contain, you can do this:
def nbr_zeroes(n)
return 1 if n == 0
m = n
i = 0
while m > 0
i += 1 if m % 10 == 0
m /= 10
end
i
end
numbers = [1, 3, 500, 200, 4000, 3000, 10000, 90, 20, 500000]
numbers.group_by { |i| nbr_zeroes(i) }
#=> { 0=>[1, 3], 2=>[500, 200], 3=>[4000, 3000], 4=>[10000] }
numbers = [100000, 100001, 304070, 3500040, 314073, 2000, 314873, 0]
numbers.group_by { |i| nbr_zeroes(i) }
#=> { 5=>[100000], 4=>[100001, 3500040], 3=>[304070, 2000],
# 1=>[314073, 0], 0=>[314873] }
Group by floor of log base 10?
1.9.3-p484 :014 > numbers.each {|n| grouped[Math.log10(n).floor] << n}
=> [1, 3, 500, 200, 4000, 3000, 10000, 90, 20, 500000]
1.9.3-p484 :016 > grouped
=> {0=>[1, 3], 2=>[500, 200], 3=>[4000, 3000], 4=>[10000], 1=>[90, 20], 5=>[500000]}
Or try 1 + Math.log10(n).floor if you need the keys to be the actual number of digits.

Resources