Carlos
Carlos

Reputation: 425

My own version of `merge` method for hashes

I am trying to create my version of the merge method for hashes. This is one of the tests:

test_hash_1 = { a: 10, b: 2 }
test_hash_2 = { a: 5, b: 10, c: 3 }
test_hash_1.my_merge(test_hash_2) { |key, oldval, newval| newval - oldval } #=> {a: -5, b: 8, c: 3}

Like Hash#merge, the code needs to return an array of all the values of one specific key. For example:

test_hash_1 = { a: 10, b: 2 }
test_hash_2 = { b: 3, c: 4 }
expect { |b| test_hash_1.my_merge(test_hash_2, &b)}.to yield_successive_args([:b, 2, 3])

This is what I have:

def my_merge(hash2, &blk)
    new_hash = self    
    if block_given?
      hash2.each do |k1, v1|
        new_hash[k1] = blk.call
      end
    else
      hash2.each do |k2, v2|
        new_hash[k2] = v2
      end
    end
    new_hash
  end
end

I have some difficulty understanding how blocks work. My code is not close to what the expected outcome should be. I would appreciate any help.

Upvotes: 1

Views: 65

Answers (2)

Stefan
Stefan

Reputation: 114228

Let's take a look at your code:

def my_merge(hash2, &blk)
  # ...
end

Since you are not going to pass the block around, you don't have to specify the block argument explicitly. You can just define it as:

def my_merge(hash2)
  # ...
end

and use yield(...) instead of blk.call(...).


You create new_hash via:

  new_hash = self

which will make new_hash[k1] = ... equivalent to self[k1] = .... To avoid modifying the receiver, create a copy via dup instead:

  new_hash = dup

Your first conditional checks whether a block is given or not. But according to the documentation, the block is only called for duplicate entries. So the actual conditional is: does the key exists and is a block given. And because it has to take the key into account, we have to move it into the each block:

  hash2.each do |k, v|
    if new_hash.key?(k) && block_given?
      new_hash[k] = yield(k, new_hash[k], v)
    else
      new_hash[k] = v
    end
  end

The 3 arguments we're passing via yield are key, old value and new value.


You may have noticed a pattern in your code:

def m(ary)
  obj = initial_value
  ary.each do |e|
    # modify obj
  end
  obj
end

This can be expressed more concise by using each_with_object:

def m(ary)
  ary.each_with_object(initial_value) do |e, o|
    # modify obj
  end
end

The whole code:

class Hash
  def my_merge(hash)
    hash.each_with_object(dup) do |(k, v), h|
      if h.key?(k) && block_given?
        h[k] = yield(k, h[k], v)
      else
        h[k] = v
      end
    end
  end
end

Upvotes: 5

Cary Swoveland
Cary Swoveland

Reputation: 110725

See Hash#merge for requirements.

class Hash
  def my_merge(h)
    keys.each_with_object({}) do |k,g|
      g[k] = if h.key?(k)
               block_given? ? yield(k, self[k], h[k]) : h[k]
             else
               self[k]
             end
    end.tap { |g| (h.keys-keys).each { |k| g[k] = h[k] } }
  end
end

h = { a: 1, b: 2, c: 3 }
g = {       b: 3, c: 4, d: 5 }        

h.my_merge(g)                 #=> {:a=>1, :b=>3, :c=>4, :d=>5}
h.merge(g)                    #=> {:a=>1, :b=>3, :c=>4, :d=>5}

h.my_merge(g) { |_,o,n| o+n } #=> {:a=>1, :b=>5, :c=>7, :d=>5}
h.merge(g)    { |_,o,n| o+n } #=> {:a=>1, :b=>5, :c=>7, :d=>5}

merge does not return arrays of values of common keys, but it can be used with a particular block to do that:

h.my_merge(g) { |_,o,v| [o, v] } #=> {:a=>1, :b=>[2, 3], :c=>[3, 4], :d=>5}
h.merge(g)    { |_,o,v| [o, v] } #=> {:a=>1, :b=>[2, 3], :c=>[3, 4], :d=>5}

For readers unfamiliar with Object#tap, without it I would need to write the method something like the following.

def my_merge(h)
  g = keys.each_with_object({}) do |k,g|
    g[k] = if h.key?(k)
             block_given? ? yield(k, self[k], h[k]) : h[k]
           else
             self[k]
           end
  end
  (h.keys-keys).each { |k| g[k] = h[k] }
  g
end

Upvotes: 1

Related Questions