Reputation: 425
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
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
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