hash merge vs assignment - ruby

I have a simple case of a hash that's passed along that is missing a field.
I'm wondering which is the preferred way of adding the field with some default and whether or not are there benefits to choosing one over the other and when?
hash['field'] = some_value
vs
hash.merge!({ 'field' => some_value })
edit: meant to use merge!

First, an important note: Hash#merge returns a new object, whereas Hash#[]= and Hash#merge! mutate the existing object:
hash = {a: :b}
hash.merge(c: :d) # => {a: :b, c: :d}
puts hash # => {a: :b} (!!!)
hash[:c] = :d
puts hash # => {a: :b, c: :d}
hash.merge!(e: :f)
puts hash # => {a: :b, c: :d, e: :f}
The main use-case for using Hash#merge! over Hash#[]= is (as the name suggests!) when you have two existing hashes that need to be merged together:
hash1 = {a: :b, c: :d}
hash2 = {e: :f, g: :h}
hash1.merge!(hash2)
puts hash1 # => {a: :b, c: :d, e: :f, g: :h}
This is equivalent to, but more convenient than, the much more verbose approach of looping through all values in hash2:
hash2.each do |key, value|
hash1[key] = value
end

I'm wondering which is the preferred way of adding the field with some default
I'd store the defaults in an extra hash (even if it is just a single value), so I can easily add other values later:
defaults = { foo: 1, bar: 2 }
Then, I'd merge the defaults in a way that preserve the given value, either by creating a new hash:
hash = { foo: 100, baz: 300 }
hash = defaults.merge(hash)
#=> {:foo=>100, :bar=>2, :baz=>300}
or by changing the hash in-place:
hash = { foo: 100, baz: 300 }
hash.merge!(defaults) { |key, given_value, default_value| given_value }
#=> {:foo=>100, :baz=>300, :bar=>2}

the first approach changes the hash itself (no new object is created)
the second creates new instance of Hash class with keys from both original hash and { 'field' => some_value } (compare object_id of hash and of result of hash.merge({ 'field' => some_value }) - they'll be different)
which one to choose?
it depends - if changing state of hash is not a problem for you, then the first approach is ok.

Related

Immutable alternative to `delete` in Ruby

Is there a version of Hash#delete as below:
hash = {a: 1}
hash.delete(:a) # => 1
hash # => {}
that returns a hash without :a, without mutating the original hash so that it would have its original value?
Use Hash#reject.
hash.reject { |k,_| k == :a }
#=> {}
hash
#=> {:a=>1}
This of course does not depend on the hash having a single key-value pair.

How to tell if there are other keys in a hash other than specified ones?

Say I have a hash:
a = {b: "asdfgh", c: "qwerty", d: "dvorak"}
And I want to be able to tell if there are keys other than the ones I specify in it, something like this:
a.has_other_keys?(:b, :c, :d)
=> false
a.has_other_keys?(:c, :d)
=> true
But I don't want it to return false if there are LESS keys than specified:
a.has_other_keys?(:b, :c, :d, :e)
=> true
Is there an easy way to do this in ruby?
Rails has an except/except! method that returns the hash with those keys removed. If you're already using Rails, You can use it
class Hash
# Returns a hash that includes everything but the given keys.
# hash = { a: true, b: false, c: nil}
# hash.except(:c) # => { a: true, b: false}
# hash # => { a: true, b: false, c: nil}
#
# This is useful for limiting a set of parameters to everything but a few known toggles:
# #person.update(params[:person].except(:admin))
def except(*keys)
dup.except!(*keys)
end
# Replaces the hash without the given keys.
# hash = { a: true, b: false, c: nil}
# hash.except!(:c) # => { a: true, b: false}
# hash # => { a: true, b: false }
def except!(*keys)
keys.each { |key| delete(key) }
self
end
end
Using above you can do:
bundle :002 > a = {b: "asdfgh", c: "qwerty", d: "dvorak"}
=> {:b=>"asdfgh", :c=>"qwerty", :d=>"dvorak"}
bundle :006 > a.except(:b)
=> {:c=>"qwerty", :d=>"dvorak"}
bundle :007 > a.except(:b).length
=> 2
bundle :008 > a.except(:b, :c, :d, :e).length
=> 0
In plain Ruby, You can do something like following:
2.2.2 :010 > a.select{|x| ![:b, :c, :d, :e].include?(x)}
=> {}
2.2.2 :011 > a.select{|x| ![:b, :c, :d, :e].include?(x)}.length
=> 0
EDIT: I just noticed Cary Swoveland's answer uses this same technique. His answer was here first.
If this doesn't have to be a high performance solution, you might consider using Array arithmetics (which a nice Ruby feature).
For example:
Your hash:
a = {b: "asdfgh", c: "qwerty", d: "dvorak"}
And our known keys:
known_keys = [:b, :c]
The rest of the keys:
other_keys = a.keys - known_keys
other_keys.empty?
For a true/false statement, we can do something like this:
(a.keys - [:b, :c]).empty?
def subset_of_keys?(h, other_keys)
(other_keys - h.keys).empty?
end
h = { b: "asdfgh", c: "qwerty", d: "dvorak"}
subset_of_keys? h, [:d, :b]
#=> true
subset_of_keys? h, [:b, :w, :d]
#=> false
See Array#-.
You can check whether sets constructed from the keys are the same:
require 'set'
class Hash
def has_other_keys?(*keys)
Set.new(keys) != Set.new(self.keys)
end
end

what is difference between store vs merge in ruby hashes?

i create a hash:
a = {}
=> {}
then:
a.store(:b, {})
=> {}
and:
a.merge!(c: {})
=> {:b=>{}, :c=>{}}
what are differences actually?
store is an assignment method.
a = {}
# => {}
a.store(:b, {})
a
# => {:b=>{}}
# Here you are assigning a key :b with empty hash {}
Another example to make it clearer:
a = {}
# => {}
a.store("key", "value")
a
# => {"key"=>"value"}
merge on the other hand manipulates your existing hash by merging with a different hash.
Example:
a = {}
# => {}
a.merge({"key" => "value"})
# => {"key"=>"value"}
a
# => {} # original value still unchanged
a.merge!({"key" => "value"})
# => {"key"=>"value"}
a
# => {"key"=>"value"} # original value updated
However unless you use merge! a's value will not get changed i.e. merge will occur only for return.
what are differences actually?
I think the main difference is merge! will let you decide which value to keep when duplicate key is provided, since it expects a block as well.
On the other hand, when you use store, the previous value will be replaced by the latest value when duplicate key is provided.
store
h1 = { "a" => 100, "b" => 200 }
h1.store("b", 254)
#=> {"a"=>100, "b"=>254}
merge!
h1 = { "a" => 100, "b" => 200 }
h2 = { "b" => 254, "c" => 300 }
h1.merge!(h2) { |key, v1, v2| v1 }
#=> {"a"=>100, "b"=>200, "c"=>300}
store takes just one key/value tuple as input and returns the stored value.
h1 = { foo: 'bar' }
h1.store(:baz, 1) #=> 1
h1 #=> { foo: 'bar', baz: 1 }
Whereas merge! accepts a hash as input and returns the updated hash:
h2 = { foo: 'bar' }
h2.merge!(baz: 1, buz: 2) #=> { foo: 'bar', baz: 1, buz: 2 }
h2 #=> { foo: 'bar', baz: 1, buz: 2 }
merge! takes one argument, which is hash to merge into original. store takes two arguments, which is key and value to store. Therefore, with merge!, you can add multiple keys to original hash, like this:
a = {}
a.merge!(a: 'a', b: 'b')
a
# => {:a => "a", :b => "b"}
For a hash h, Hash#store has the same effect as Hash#[]=: they both either add one key-value pair k=>v to h (if h does not have a key k) or modify the value of key k (if the hash already contains the key). Also, they both return v.
Hash#merge! (aka update) has two forms. The first does the same thing as store, except it does it for each key-value pair in another hash. The second form uses a block to determine the values of keys that are present in both hashes being merged. Please refer to the docs for details on that form of the method. Both forms of merge! return the "merged" hash.
Hash#merge is not a relevant comparison as it does not mutate the hash.

Cloning a Hash in Ruby2 [duplicate]

This question already has answers here:
How to create a deep copy of an object in Ruby?
(9 answers)
Closed 8 years ago.
Im trying to clone a hash, to make a new copy of the original hash but it seems that when I set a value in the new hash, I have the same effect on the original hash.
rr = Hash.new
command = "/usr/local/bin/aws route53 list-resource-record-sets --hosted-zone-id EXAMPLEID --max-items 1"
rr=JSON.parse(%x{#{command}})
puts rr
if rr["ResourceRecordSets"][0]["TTL"] != 60
new_rr = rr.clone
new_rr["ResourceRecordSets"][0]["TTL"] = 60
puts rr
puts new_rr
end
Output:
{"NextRecordType"=>"MX", "NextRecordName"=>"example.com.", "ResourceRecordSets"=>[{"ResourceRecords"=>[{"Value"=>"1.2.3.4"}], "Type"=>"A", "Name"=>"example.com.", "TTL"=>1800}], "MaxItems"=>"1", "IsTruncated"=>true}
{"NextRecordType"=>"MX", "NextRecordName"=>"example.com.", "ResourceRecordSets"=>[{"ResourceRecords"=>[{"Value"=>"1.2.3.4"}], "Type"=>"A", "Name"=>"example.com.", "TTL"=>60}], "MaxItems"=>"1", "IsTruncated"=>true}
{"NextRecordType"=>"MX", "NextRecordName"=>"example.com.", "ResourceRecordSets"=>[{"ResourceRecords"=>[{"Value"=>"1.2.3.4"}], "Type"=>"A", "Name"=>"example.com.", "TTL"=>60}], "MaxItems"=>"1", "IsTruncated"=>true}
I dont see Hash.clone documented in Ruby 2.0, should I be using another method to create a Hash copy now?
Thanks in advance.
Hash is a collection of keys and values, where values are references to objects. When duplicating a hash, new hash is being created, but all object references are being copied, so as result you get new hash containing the same values. That is why this will work:
hash = {1 => 'Some string'} #Strings are mutable
hash2 = hash.clone
hash2[1] #=> 'Some string'
hash2[1].upcase! # modifying mutual object
hash[1] #=> 'SOME STRING; # so it appears modified on both hashes
hash2[1] = 'Other string' # changing reference on second hash to another object
hash[1] #=> 'SOME STRING' # original obejct has not been changed
hash2[2] = 'new value' # adding obejct to original hash
hash[2] #=> nil
If you want duplicate the referenced objects, you need to perform deep duplication. It is added in rails (activesupport gem) as deep_dup method. If you are not using rails and don;t want to install the gem, you can write it like:
class Hash
def deep_dup
Hash[map {|key, value| [key, value.respond_to?(:deep_dup) ? value.deep_dup : begin
value.dup
rescue
value
end]}]
end
end
hash = {1 => 'Some string'} #Strings are mutable
hash2 = hash.deep_dup
hash2[1] #=> 'Some string'
hash2[1].upcase! # modifying referenced object
hash2[1] #=> 'SOME STRING'
hash[1] #=> 'Some string; # now other hash point to original object's clone
You probably should write something similar for arrays. I would also thought about writing it for whole enumerable module, but it might be slightly trickier.
The easiest way to make a deep copy of most Ruby objects (including strings, arrays, hashes and combinations thereof) is to use Marshal:
def deep_copy(obj)
Marshal.load(Marshal.dump(obj))
end
For example,
h = {a: 1, b: [:c, d: {e: 4}]} # => {:a=>1, :b=>[:c, {:d=>{:e=>4}}]}
hclone = h.clone
hdup = h.dup
hmarshal = deep_copy(h)
h[:b][1][:d][:e] = 5
h # => {:a=>1, :b=>[:c, {:d=>{:e=>5}}]}
hclone # => {:a=>1, :b=>[:c, {:d=>{:e=>5}}]}
hdup # => {:a=>1, :b=>[:c, {:d=>{:e=>5}}]}
hmarshal # => {:a=>1, :b=>[:c, {:d=>{:e=>4}}]}

Deleting multiple key and value pairs from hash in Rails

number = {:a => 1, :b => 2, :c => 3, :d => 4}
upon evaluation of certain condition i want to delete key-value pair of a,b,c
number.delete "A"
number.delete "B"
number.delete "C"
Or, less performant but more terse:
number.reject! {|k, v| %w"A B C".include? k }
or, more performant than second Chris' solution but shorter than first:
%w"A B C".each{|v| number.delete(v)}
ActiveSupport that is a part of Rails comes with several built-in methods can help you to achieve your goal.
If you just want to delete some key-value pairs, you can use Hash#except!
number.except!(:a, :b, :c)
If you want to keep the original hash, then use Hash#except
new_hash = number.except!(:a, :b, :c)
new_hash # => {:d=>4}
number # => {:a=>1, :b=>2, :c=>3, :d=>4}
You also can go with Rails-free way:
new_hash = number.dup.tap do |hash|
%i[a b c].each {|key| hash.delete(key)}
end
new_hash # => {:d=>4}
number # => {:a=>1, :b=>2, :c=>3, :d=>4}
P.S.: the last code example is very slow, I'm just providing it as an alternative.

Resources