Easily map over Hash like Array#map - ruby

Sometimes I want to map over a collection.
If it's an array it's easy:
foo = [1,2,3]
foo.map {|v| v + 1}
#=> [2, 3, 4]
But a hash doesn't work the same way:
bar = {a: 1, b: 2, c: 3}
bar.map{|k,v| v+1}
#=> [2, 3, 4]
What I'd really like is something like:
bar = {a: 1, b: 2, c: 3}
bar.baz{|k,v| v+1}
#=> {:a=>2, :b=>3, :c=>4}
where Hash#baz is some method. Is there an easy way to get a "map-like" experience for a hash?

In Ruby 2.4 you can use the built-in Hash#transform_values:
bar = {a: 1, b: 2, c: 3}
# => {:a=>1, :b=>2, :c=>3}
bar.transform_values {|v| v+1 }
# => {:a=>2, :b=>3, :c=>4}

Just to point out the obvious and most common solution to address this need:
bar = {a: 1, b: 2, c: 3}
# => {:a=>1, :b=>2, :c=>3}
bar.map { |k, v| [k, v + 1] }.to_h
# => {:a=>2, :b=>3, :c=>4}

From the ruby-forum, you can use Hash#merge to merge the hash with itself:
bar = {a: 1, b: 2, c: 3}
#=> {:a=>1, :b=>2, :c=>3}
bar.merge(bar){|k,v| v+1}
#=> {:a=>2, :b=>3, :c=>4}

One of the great things about Ruby is if you don't like what's in the Ruby core you can always go and extend it to fill in the missing pieces:
class Hash
def map_values
map do |k,v|
[ k, yield(k, v) ]
end.to_h
end
end
Which gives you the thing you wanted:
bar = {a: 1, b: 2, c: 3}
# => {:a=>1, :b=>2, :c=>3}
bar.map_values{ |k,v| v+1 }
# => {:a=>2, :b=>3, :c=>4}
I'm surprised this hasn't been introduced into core Ruby, but it might be in the future.
Update: As Eric points out, transform_values is now in Ruby 2.4.0. This is also in ActiveSupport if you're using Rails 4.2 or later.
bar = {a: 1, b: 2, c: 3}
# => {:a=>1, :b=>2, :c=>3}
bar.transform_values{ |v| v+1 }
# => {:a=>2, :b=>3, :c=>4}

bar = {a: 1, b: 2, c: 3}
bar.merge(bar) { |*,v| v+1 }
#=> {:a=>2, :b=>3, :c=>4}
This uses the form of Hash.merge that employs a block to return the values of keys that are present in both hashes being merged, which here is all keys.
Another way:
bar.keys.each { |k| bar[k] += 1 }
bar
#=> {:a=>2, :b=>3, :c=>4}
which can be written in one line using Object#tap:
bar.tap { |h| h.keys.each { |k| h[k] += 1 } }
#=> {:a=>2, :b=>3, :c=>4}

Related

Ruby - Merge an Array into a Hash

I have an array which looks like this:
array = [[:foo, :bar], [:foo, :baz], [:baz, {a: 1, b: 2}], [:baz, {c: 1, d:2}]]
and I need to turn it into a hash which looks like this:
{:foo =>[:bar, :baz], :baz => {a: 1, b: 2, c: 1, d: 2}}
This is the code I have so far:
def flatten(array)
h = {}
array.each_with_object({}) do |(k, v), memo|
if v.is_a?(Hash)
memo[k] = h.merge!(v)
else
# What goes here?
end
end
end
When used like so:
flatten(array)
outputs:
{baz => {:a => 1, :b => 2, :c => 1, :d => 2}}
May someone please point me in the right direction? Help appreciated.
def convert(arr)
arr.each_with_object({}) do |a,h|
h[a.first] =
case a.last
when Hash
(h[a.first] || {}).update(a.last)
else
(h[a.first] || []) << a.last
end
end
end
convert array
#=> {:foo=>[:bar, :baz], :baz=>{:a=>1, :b=>2, :c=>1, :d=>2}}
Hash[ array.group_by(&:first).map{ |k,a| [k,a.map(&:last)] } ]
Here is my attempt at solving this problem. I have to make assumption that in the input array, entries like the ones similar to :baz will always be paired with Hash objects. The solution will not work if you have one :baz with a symbol and another with hash.
array = [[:foo, :bar], [:foo, :baz], [:baz, {a: 1, b: 2}], [:baz, {c: 1, d:2}]]
h = Hash.new
array.each do |n1, n2|
if n2.class == Hash
h[n1] = (h[n1] || {}).merge(n2)
else
h[n1] = (h[n1] || []) << n2
end
end
p h
Output
{:foo=>[:bar, :baz], :baz=>{:a=>1, :b=>2, :c=>1, :d=>2}}
[Finished in 0.1s]

Convert string values to Hash

I have a string that I need to convert into a hash. The string's key will always be a symbol and the value will always be an integer:
"a=1, b=2, c=3, d=4"
This string should return a hash that looks like:
{ :a => 1, :b => 2, :c => 3, :d => 4 }
I've tried several different things, but the closest I've been able to come so far is to split the string twice, first for the comma and space, second for the equal sign, and create symbols:
def str_to_hash(str)
array = str.split(', ').map{|str| str.split('=').map{|k, v| [k.to_sym, v] }}
end
I'd expected the following output:
{:a=>1, :b=>2, :c=>3, :d=>4}
Instead I got:
[[[:a, nil], [:"1", nil]], [[:b, nil], [:"2", nil]], [[:c, nil], [:"3", nil]], [[:d, nil], [:"4", nil]]]
As you can see, it is creating 8 separate strings with 4 symbols. I can't figure out how to make Ruby recognize the numbers and set them as the values in the key/value pair. I've looked online and even asked my coworkers for help, but haven't found an answer so far. Can anybody help?
Try this I think it looks a little cleaner
s= "a=1, b=2, c=3, d=4"
Hash[s.scan(/(\w)=(\d)/).map{|a,b| [a.to_sym,b.to_i]}]
Here is the inner workings
#utilize scan with capture groups to produce a multidimensional Array
s.scan(/(\w)=(\d)/)
#=> [["a", "1"], ["b", "2"], ["c", "3"], ["d", "4"]]
#pass the inner Arrays to #map an replace the first item with a sym and the second to Integer
.map{|a,b| [a.to_sym,b.to_i]}
#=> [[:a, 1], [:b, 2], [:c, 3], [:d, 4]]
#Wrap the whole thing in Hash::[] syntax to convert
Hash[s.scan(/(\w)=(\d)/).map{|a,b| [a.to_sym,b.to_i]}]
#=> {:a=>1, :b=>2, :c=>3, :d=>4}
If you want to avoid the Hash::[] method which I have always though was ugly you can do the following
#Ruby >= 2.1 you can use Array#to_h
s.scan(/(\w)=(\d)/).map{|a,b| [a.to_sym,b.to_i]}.to_h
#=> {:a=>1, :b=>2, :c=>3, :d=>4}
#Ruby < 2.1 you can use Enumerable#each_with_object
s.scan(/(\w)=(\d)/).each_with_object({}){|(k,v),obj| obj[k.to_sym] = v.to_i}
#=> {:a=>1, :b=>2, :c=>3, :d=>4}
Although there are a ton of other ways to handle this issue as is evident by the many other answers here is one more just for fun.
Hash[*s.scan(/(\w)=(\d)/).flatten.each_with_index.map{|k,i| i.even? ? k.to_sym : k.to_i}]
> s = "a=1, b=2, c=3, d=4"
=> "a=1, b=2, c=3, d=4"
> Hash[s.split(",").map(&:strip).map { |p| p.split("=") }.map { |k, v| [ k.to_sym, v.to_i ] }]
=> {:a=>1, :b=>2, :c=>3, :d=>4}
Part of the problem is that you're trying to do it in a single line and losing track of what the intermediate values are. Break it down into each component, make sure you're using what Ruby gives you, etc.
Your naming assumes you get an array back (not a hash). Hash[...], however, will create a hash based on an array of [key, value] pairs. This makes manual hash stuffing go away. Also, that method should return a hash, not set something–keep methods small, and pure.
Note I strip the first set of split values. This avoids symbols like :" a", which you get if you don't trim leading/trailing spaces. My code does not take strings like "a = 1" into account–yours should.
First, make things readable. Then, if (and only if) it makes sense, and remains legible, play code golf.
> s = "a=1, b=2, c=3, d=4"
=> "a=1, b=2, c=3, d=4"
> a1 = s.split(",")
=> ["a=1", " b=2", " c=3", " d=4"]
> a2 = a1.map(&:strip)
=> ["a=1", "b=2", "c=3", "d=4"]
> a3 = a2.map { |s| s.split("=") }
=> [["a", "1"], ["b", "2"], ["c", "3"], ["d", "4"]]
> a4 = a3.map { |k, v| [ k.to_sym, v.to_i ] }
=> [[:a, 1], [:b, 2], [:c, 3], [:d, 4]]
> Hash[a4]
=> {:a=>1, :b=>2, :c=>3, :d=>4}
Unrelated, but if you're doing a lot of ETL with Ruby, especially on plain text, using mixins can make code much cleaner, closer to a DSL. You can play horrible games, too, like:
> def splitter(sym, s)
String.send(:define_method, sym) do
split(s).map(&:strip)
end
end
> s = "a=1, b=2, c=3, d=4"
> splitter :split_comma, ","
> splitter :split_eq, "-"
> Hash[s.split_comma.map(&:split_eq).map { |k, v| [ k.to_sym, v.to_i ]}]
=> {:a=>1, :b=>2, :c=>3, :d=>4}
It can get significantly worse than this and become a full-fledged ETL DSL. It's great if you need it, though.
Have you tried Hash['a',1,'b',2,'c',3] ??
On your irb terminal it should give this => {"a"=>1, "b"=>2, "c"=>3}
So all that you can do is split the string and give it to Hash which will do the job for you.
Hash[s.split(",").map(&:strip).map{|p| x = p.split("="); [x[0].to_sym, x[1]]}]
Hope that helps
a little hackhish but it works, you can take it from here :-)
h = {}
"a=1, b=2, c=3, d=4".split(',').each do |fo|
k = fo.split('=').first.to_sym
v = fo.split('=').last
h[k] = v
end
puts h.class.name
puts h
My solution:
string = "a=1, b=2, c=3, d=4"
hash = {}
string.split(',').each do |pair|
key,value = pair.split(/=/)
hash[key] = value
end
puts hash.inspect
Despite from not being a one-linner it's a readable solution.

ruby: remove a value in an array that is in a hash

I have a hash like this:
a = { a: 1, b: 2, c: [9, 8, 7]}
I need to write a method that given a pair key and value, removes the occurrences of such couple from the hash.
for example, if I pass the couple (:a, 1) I obtain the hash:
a = { b: 2, c: [9, 8, 7]}
if I pass the couple (:c, 8) I obtain the hash:
a = { a: 1, b: 2, c: [9, 7]}
if I pass the couple (:a, 3) I obtain the (unchanged) hash:
a = { a: 1, b: 2, c: [9, 8, 7]}
I'm not sure how to do this, here's what I got so far:
def remove_criterion (key, value)
all_params = params.slice(key)
if all_params[key].class == Array
else
params.except(key)
end
end
which obviously is incomplete.
thanks for any help,
Here's one solution:
def remove_criterion key, value
params.each_with_object({}) do |pair, h|
k, v = *pair
if k == key
case v
when Array
nv = v.reject { |each| each == value }
h[k] = nv unless nv.empty?
else
h[k] = v unless v == value
end
else
h[k] = v
end
end
end
Testing it out in irb:
irb(main):007:0> remove_criterion :a, 1
=> {:b=>2, :c=>[9, 8, 7]}
irb(main):008:0> remove_criterion :c, 8
=> {:a=>1, :b=>2, :c=>[9, 7]}
irb(main):009:0> remove_criterion :a, 3
=> {:a=>1, :b=>2, :c=>[9, 8, 7]}
def remove_criterion(key, value)
params.each do |k,v|
if k == key and v == value
params.delete(key)
elsif v.class == Array and v.include?(value)
v.delete(value)
end
end
params
end
I'd do it like this:
def doit(h,k,v)
return h unless h.include?(k)
if h[k] == v
h.delete(k)
elsif h[k].is_a? Array
h[k].delete(v)
end
h
end
h = {a: 1, b: 2, c: [9, 8, 7]}
doit(h,:b,2) # => {:a=>1, :c=>[9, 8, 7]}
doit(h,:b,3) # => {:a=>1, :b=>2, :c=>[9, 8, 7]}
doit(h,:c,8) # => {:a=>1, :b=>2, :c=>[9, 7]}
doit(h,:c,6) # => {:a=>1, :b=>2, :c=>[9, 8, 7]}
doit(h,:d,1) # => {:a=>1, :b=>2, :c=>[9, 8, 7]}

Ruby creating Hash custom invert function in Ruby

Ruby class Hash has method "invert" which make "reversal" between keys and values and delete same keys (in our case its: "1=>:a").
h = {a: 1, b: 2, c: 1}
=> {:a=>1, :b=>2, :c=>1}
h.invert
=> {1=>:c, 2=>:b}
How implement custom Hash method "c_invert", which will return very first (not last) pair of duplicated key => value? Exapmle:
> h = {a: 1, b: 2, c: 1}
=> {:a=>1, :b=>2, :c=>1}
> h.c_invert
=> {1=>:a, 2=>:b}
class Hash
def c_invert
Hash[to_a.reverse].invert
end
end
or
class Hash
def c_invert
Hash[to_a.reverse.map(&:reverse)]
end
end
h = {:d =>1,:a=>1, :b=> 2, :c=>1}
Hash[h.map(&:reverse).reverse]
# => {1=>:d, 2=>:b}
h = {a: 1, b: 2, c: 1}
Hash[h.map(&:reverse).reverse]
# => {1=>:a, 2=>:b}

Converting an array of keys and an array of values into a hash in Ruby

I have two arrays like this:
keys = ['a', 'b', 'c']
values = [1, 2, 3]
Is there a simple way in Ruby to convert those arrays into the following hash?
{ 'a' => 1, 'b' => 2, 'c' => 3 }
Here is my way of doing it, but I feel like there should be a built-in method to easily do this.
def arrays2hash(keys, values)
hash = {}
0.upto(keys.length - 1) do |i|
hash[keys[i]] = values[i]
end
hash
end
The following works in 1.8.7:
keys = ["a", "b", "c"]
values = [1, 2, 3]
zipped = keys.zip(values)
=> [["a", 1], ["b", 2], ["c", 3]]
Hash[zipped]
=> {"a"=>1, "b"=>2, "c"=>3}
This appears not to work in older versions of Ruby (1.8.6). The following should be backwards compatible:
Hash[*keys.zip(values).flatten]
Another way is to use each_with_index:
hash = {}
keys.each_with_index { |key, index| hash[key] = values[index] }
hash # => {"a"=>1, "b"=>2, "c"=>3}
The same can be done using Array#transpose method. If you are using Ruby version >= 2.1, you can take the advantage of the method Array#to_h, otherwise use your old friend, Hash::[]
keys = ['a', 'b', 'c']
values = [1, 2, 3]
[keys, values].transpose.to_h
# => {"a"=>1, "b"=>2, "c"=>3}
Hash[[keys, values].transpose]
# => {"a"=>1, "b"=>2, "c"=>3}
Try this, this way the latter one d will overwrite the former one c
irb(main):001:0> hash = Hash[[[1,2,3,3], ['a','b','c','d']].transpose]
=> {1=>"a", 2=>"b", 3=>"d"}
irb(main):002:0>

Resources