4

I am trying to create an array/hash from an array of multiple hashes with same keys and an average of values. My array:

[{:amount=>897500, :gross_amount=>897500, :tax=>147500, :hotel_fees=>0, :base_fare=>750000, :currency=>"INR"}, {:amount=>1006500, :gross_amount=>1006500, :tax=>156500, :hotel_fees=>0, :base_fare=>850000, :currency=>"INR"}]

Now I want to return something like this:

{:amount=>952000, :gross_amount=>952000, :tax=>152000, :hotel_fees=>0, :base_fare=>800000, :currency=>"INR"}

where values are the average of values from each hash with same key.

Is there a simple way to do this. I have tried using merge but currency becomes 0 with it.

My attempt:

p[0].merge(p[1]){|k,v1,v2| (v1+v2)/2 unless v1 && v2 == "INR"}

Edit:

Actually my problem didn't end here, so after getting the average I needed to insert the values inside another hash. So I used something like this:

        price_array = offer_values.map do |v| 
          v.inject do |k, v| 
            k.merge!(price: k[:price].merge(v[:price]){|_, a, b| [a, b].flatten })
          end
        end
        price_array.map do |o|
          o[:price] = {}.tap{ |h| o[:price].each {|k, list| h[k] = list.all?{|e| [Fixnum, NilClass].include? e.class} ? list.map(&:to_i).sum/list.size : list.compact.first ; h  } }
        end

Where offer_array is the one with my orginal/first array in separate hashes. This I have tried for with 2 and 3 hashes and it is working.

If you guys have any suggestion on improving the code, It am open.

Ravi
  • 304
  • 2
  • 15

4 Answers4

3

Irb

2.2.3 :011 > b = {test1: 30, test2: 40}
 => {:test1=>30, :test2=>40} 
2.2.3 :012 > a = {test1: 20, test2: 60}
 => {:test1=>20, :test2=>60} 
2.2.3 :013 > c = a.merge(b){|key, oldval, newval| (newval + oldval)/2}
 => {:test1=>25, :test2=>50} 
Ramon Marques
  • 3,046
  • 2
  • 23
  • 34
2

The accepted answer will not work for more than 2 hashes, since merge works only 2 by 2 and you are calculating average here.

(((3 + 2) / 2) + 2.5) / 2 is different from (3 + 2 + 2.5) / 3

So I wrote a piece of code that could do what you want for whatever size of array you have

  def self.merge_all_and_average(array)
    new_hash = {}
    unless array.empty?
      array[0].keys.each do |key|
        if array[0][key].class == Fixnum
          total = array.map { |i| i[key] }.inject(0) { |sum, x| sum + x }
          new_hash = new_hash.merge(key => total / array.size)
        else
          new_hash = new_hash.merge(key => array[0][key])
        end
      end
    end
    new_hash
  end
Ramon Marques
  • 3,046
  • 2
  • 23
  • 34
  • I know it won't work for more than two hash. I just wanted an idea on how I could handle it. I was on the right path but couldn't get correct answer. My problem never was totally different and various other things were involved. This was just a part of it where I was getting confused. The solution I have used is somewhat similar to yours. Check my edit. – Ravi Jun 14 '17 at 17:19
2

This should work with any number of hashes:

data = [
  { amount: 897_500, gross_amount: 897_500, tax: 147_500, hotel_fees: 0, base_fare: 750_000, currency: 'INR' },
  { amount: 1_006_500, gross_amount: 1_006_500, tax: 156_500, hotel_fees: 0, base_fare: 850_000, currency: 'INR' },
  { amount: 1_006_500, gross_amount: 1_006_500, tax: 156_500, hotel_fees: 0, base_fare: 850_000, currency: 'INR' }
]

transposed_hashes = data.each_with_object(Hash.new{|h, k| h[k] = []}) do |h, mem|
  h.each do |k, v|
    mem[k] << v
  end
end
# {:amount=>[897500, 1006500, 1006500], :gross_amount=>[897500, 1006500, 1006500], :tax=>[147500, 156500, 156500], :hotel_fees=>[0, 0, 0], :base_fare=>[750000, 850000, 850000], :currency=>["INR", "INR", "INR"]}

average_hash = transposed_hashes.map do |k, v|
  new_value = if v[0].is_a? Integer
                v.sum.to_f / v.size
              else
                v[0]
              end
  [k, new_value]
end.to_h

puts average_hash
# {:amount=>970166.6666666666, :gross_amount=>970166.6666666666, :tax=>153500.0, :hotel_fees=>0.0, :base_fare=>816666.6666666666, :currency=>"INR"}
Eric Duminil
  • 52,989
  • 9
  • 71
  • 124
  • I am doing something similar as well but this method increases my complexity of the code. – Ravi Jun 14 '17 at 17:39
1

For two hashes in array you could use inject and merge checking if the value for the both currency keys are Fixnum class, if not, then take the value of currency "INR" in the first hash and use it:

array = [
  {:amount=>897500,  :gross_amount=>897500,  :tax=>147500, :hotel_fees=>0, :base_fare=>750000, :currency=>"INR"}, 
  {:amount=>1006500, :gross_amount=>1006500, :tax=>156500, :hotel_fees=>0, :base_fare=>850000, :currency=>"INR"}
]

p array.inject{|k,v| k.merge(v){|_,a,b| [a,b].all?{|e| e.is_a?(Fixnum)} ? (a+b)/2 : b}}
# => {:amount=>952000, :gross_amount=>952000, :tax=>152000, :hotel_fees=>0, :base_fare=>800000, :currency=>"INR"}

For two or more hashes in an array you could try with:

main_array = [
  {:amount=>897500,  :gross_amount=>897500,  :tax=>147500, :hotel_fees=>0, :base_fare=>750000, :currency=>"INR"}, 
  {:amount=>1006500, :gross_amount=>1006500, :tax=>156500, :hotel_fees=>0, :base_fare=>850000, :currency=>"INR"},
  {:amount=>1006500, :gross_amount=>1006500, :tax=>156500, :hotel_fees=>0, :base_fare=>850000, :currency=>"INR"},
]
array_result = main_array.flat_map(&:to_a).group_by(&:first).map do |key, array| 
  { 
    key => (
      result = array.inject(0) do |total, (_, value)| 
        value.is_a?(Fixnum) ? total + value : value
      end 
      result.is_a?(Fixnum) ? result / main_array.size : result 
    ) 
  } 
end
p array_result
Sebastián Palma
  • 32,692
  • 6
  • 40
  • 59
  • What is `_` in `|_,a,b|` doing? Can you please explain? – Ravi Jun 13 '17 at 16:17
  • @Ravi it's explained in my answer, first attribute is key, he called it _ second attribute is the old value, or value from the first hash, third attribute is the new value or value from the second hash – Ramon Marques Jun 13 '17 at 16:24
  • `_` corresponds to the hash's key, it could be named as you want, but as I'm not using it but it must be references then I assign it as `_`, that means that's something that's needed to make the block works, but won't be used inside. – Sebastián Palma Jun 13 '17 at 16:29
  • The question mentions "multiple hashes", not just 2 hashes. – Eric Duminil Jun 13 '17 at 21:19