3

For example, I have

array = [ {name: 'robert', nationality: 'asian', age: 10},
          {name: 'robert', nationality: 'asian', age: 5},
          {name: 'sira', nationality: 'african', age: 15} ]

I want to get the result as

array = [ {name: 'robert', nationality: 'asian', age: 15},
          {name: 'sira', nationality: 'african', age: 15} ]

since there are 2 Robert's with the same nationality.

Any help would be much appreciated.

I have tried Array.uniq! {|e| e[:name] && e[:nationality] } but I want to add both numbers in the two hashes which is 10 + 5

P.S: Array can have n number of hashes.

Robin
  • 69
  • 9
  • As a style note, we usually reserve capitalized names for types and constants in Ruby, so using `Array` for this data structure is a bit unidiomatic. – Silvio Mayolo Jun 25 '17 at 17:31
  • Totally Makes sense. I am changing the question right away. Thanks much. @Silvio Mayolo – Robin Jun 25 '17 at 17:42

3 Answers3

3

I would start with something like this:

array = [ 
  { name: 'robert', nationality: 'asian', age: 10 },
  { name: 'robert', nationality: 'asian', age: 5 },
  { name: 'sira', nationality: 'african', age: 15 } 
]
array.group_by { |e| e.values_at(:name, :nationality) }
     .map { |_, vs| vs.first.merge(age: vs.sum { |v| v[:age] }) }
#=> [
#     {
#       :name        => "robert",
#       :nationality => "asian",
#       :age         => 15
#     }, {
#       :name        => "sira",
#       :nationality => "african",
#       :age         => 15
#     }
#   ]
spickermann
  • 100,941
  • 9
  • 101
  • 131
  • Awesome! This is something that I am looking for. Thanks a lot! – Robin Jun 25 '17 at 17:33
  • How is `merge` working here, specifically the argument you pass it? – Sagar Pandya Jun 25 '17 at 18:11
  • 2
    @sagarpandya82 when a hash is the last (or only) parameter to a method the `{}` around it can be omitted, so `merge(age: vs.sum { |v| v[:age] })` is `merge` with a hash with a single key, `:age`, and sum of the ages as the value. Is that the part you were asking about? – mikej Jun 25 '17 at 19:08
  • 1
    @mikej Thank you, that was exactly what I wasn't understanding. [I really should know this](https://stackoverflow.com/a/43265783/5101493) :P. – Sagar Pandya Jun 25 '17 at 19:39
3

Let's take a look at what you want to accomplish and go from there. You have a list of some objects, and you want to merge certain objects together if they have the same ethnicity and name. So we have a key by which we will merge. Let's put that in programming terms.

key = proc { |x| [x[:name], x[:nationality]] }

We've defined a procedure which takes a hash and returns its "key" value. If this procedure returns the same value (according to eql?) for two hashes, then those two hashes need to be merged together. Now, what do we mean by "merge"? You want to add the ages together, so let's write a merge function.

merge = proc { |x, y| x.dup.tap { |x1| x1[:age] += y[:age] } }

If we have two values x and y such that key[x] and key[y] are the same, we want to merge them by making a copy of x and adding y's age to it. That's exactly what this procedure does. Now that we have our building blocks, we can write the algorithm.

We want to produce an array at the end, after merging using the key procedure we've written. Fortunately, Ruby has a handy function called each_with_object which will do something very nice for us. The method each_with_object will execute its block for each element of the array, passing in a predetermined value as the other argument. This will come in handy here.

result = array.each_with_object({}) do |x, hsh|
  # ...
end.values

Since we're using keys and values to do the merge, the most efficient way to do this is going to be with a hash. Hence, we pass in an empty hash as the extra object, which we'll modify to accumulate the merge results. At the end, we don't care about the keys anymore, so we write .values to get just the objects themselves. Now for the final pieces.

if hsh.include? key[x]
  hsh[ key[x] ] = merge.call hsh[ key[x] ], x
else
  hsh[ key[x] ] = x
end

Let's break this down. If the hash already includes key[x], which is the key for the object x that we're looking at, then we want to merge x with the value that is currently at key[x]. This is where we add the ages together. This approach only works if the merge function is what mathematicians call a semigroup, which is a fancy way of saying that the operation is associative. You don't need to worry too much about that; addition is a very good example of a semigroup, so it works here.

Anyway, if the key doesn't exist in the hash, we want to put the current value in the hash at the key position. The resulting hash from merging is returned, and then we can get the values out of it to get the result you wanted.

key = proc { |x| [x[:name], x[:nationality]] }
merge = proc { |x, y| x.dup.tap { |x1| x1[:age] += y[:age] } }
result = array.each_with_object({}) do |x, hsh|
  if hsh.include? key[x]
    hsh[ key[x] ] = merge.call hsh[ key[x] ], x
  else
    hsh[ key[x] ] = x
  end
end.values

Now, my complexity theory is a bit rusty, but if Ruby implements its hash type efficiently (which I'm fairly certain it does), then this merge algorithm is O(n), which means it will take a linear amount of time to finish, given the problem size as input.

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
2
array.each_with_object(Hash.new(0)) { |g,h| h[[g[:name], g[:nationality]]] += g[:age] }.
      map { |(name, nationality),age| { name:name, nationality:nationality, age:age } }
  [{ :name=>"robert", :nationality=>"asian", :age=>15 },
   { :name=>"sira", :nationality=>"african", :age=>15 }]

The two steps are as follows.

a = array.each_with_object(Hash.new(0)) { |g,h| h[[g[:name], g[:nationality]]] += g[:age] }
  #=> { ["robert", "asian"]=>15, ["sira", "african"]=>15 }

This uses the class method Hash::new to create a hash with a default value of zero (represented by the block variable h). Once this hash heen obtained it is a simple matter to construct the desired hash:

a.map { |(name, nationality),age| { name:name, nationality:nationality, age:age } }
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100