Why can't I multiply a hash value? - ruby

hash = {:one => 1, :two => 2, :three => 3}
new_hash = hash.each { |k, v| v.to_int * 5 }
new_hash
I want this output:
=> {"one"=>5, "two"=>10, "three"=>15}
But I get this:
=> {"one"=>1, "two"=>2, "three"=>3}
Why?

each returns the original hash. You have to build a new one:
new_hash = hash.map { |k, v| [k, v * 5] }.to_h
#=> {:one=>5, :two=>10, :three=>15}
Or for Ruby < 2.1
new_hash = Hash[hash.map { |k, v| [k, v * 5] }]
#=> {:one=>5, :two=>10, :three=>15}

As you can see in the documentation, Hash#each returns the original hash, which you then assign to new_hash.
Do this instead:
hash = {:one => 1, :two => 2, :three => 3}
new_hash = Hash[hash.map { |k, v| [k, v * 5] }]
new_hash # => {:one=>5, :two=>10, :three=>15}

You can do as follows:
hash = {:one => 1, :two => 2, :three => 3}
new_hash = hash.reduce({}) { |h, (k, v) | h[ k ] = v.to_i * 5 ; h }
# => {:one=>5, :two=>10, :three=>15}

When a block is given, each method of Hash returns the original hash with no modification. See ruby doc.

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]

Reverse mapping an array of hash to a hash

I have an Array that contains Hash with only one key and value as anArray.
Eg:
a = [{1 => ["foo", "bar"]}, {2 => ["hello"]}, {3 => ["world", "bar"]}]
Now I want to create a Hash having values of above Hashes as key and their keys as values.
Eg: (desired result)
res = {"foo"=>[1], "bar"=>[1, 3], "hello"=>[2], "world"=>[3]}
I have solved this is following way:
b = Hash.new { |h, k| h[k] = [] }
a.each { |hash|
hash.each { |key, vals|
vals.each { |val|
b[val] << key
}
}
}
b
# => {"foo"=>[1], "bar"=>[1, 3], "hello"=>[2], "world"=>[3]}
It works fine but there should be a better, shorter way to do this than iterating so many times. Please suggest.
One could invert keys and values and then collect elements as needed:
a = [{1 => ["foo", "bar"]}, {2 => ["hello"]}, {3 => ["world", "bar"]}]
a.map(&:invert).inject({}) { |memo,el|
el = el.flatten # hash of size 1 ⇒ array [k ⇒ v]
el.first.each { |k| (memo[k] ||= []) << el.last }
memo
}
#⇒ {
# "bar" => [
# [0] 1,
# [1] 3
# ],
# "foo" => [
# [0] 1
# ],
# "hello" => [
# [0] 2
# ],
# "world" => [
# [0] 3
# ]
# }
Hope it helps.
I'd use group_by, but you need to massage your data:
a.flat_map(&:to_a) # => [[1, ["foo", "bar"]], [2, ["hello"]], [3, ["world", "bar"]]]
.flat_map{|key, values| values.map{|v| [key, v]}} # => [[1, "foo"], [1, "bar"], [2, "hello"], [3, "world"], [3, "bar"]]
.group_by(&:last) # => {"foo"=>[[1, "foo"]], "bar"=>[[1, "bar"], [3, "bar"]], "hello"=>[[2, "hello"]], "world"=>[[3, "world"]]}
.map{|key, values| [key, values.map(&:first)]} # => [["foo", [1]], ["bar", [1, 3]], ["hello", [2]], ["world", [3]]]
.to_h # => {"foo"=>[1], "bar"=>[1, 3], "hello"=>[2], "world"=>[3]}

How do I intersect one hash's keys with another and filter out the matching values?

I got this from the Rails doc:
{1 => 2}.diff(1 => 2) # => {}
{1 => 2}.diff(1 => 3) # => {1 => 2}
{}.diff(1 => 2) # => {1 => 2}
{1 => 2, 3 => 4}.diff(1 => 2) # => {3 => 4}
This is almost perfect but I don't want values that are in the hash passed as parameter and not in the calling hash.
What I want:
{}.diff(1 => 2) # => {}
{a: 1}.diff({a: 1, b: 2}) # => {} instead of {:b => 2}
Also, it must be as efficient as possible. For instance, I don't want to go over the second hash and check that each key that's inside doesn't appear in the first.
Any ideas?
Looking at the source is helpful here.
def diff(h2)
dup.delete_if { |k, v| h2[k] == v }.merge!(h2.dup.delete_if { |k, v| has_key?(k) })
end
Which iterates over every entry in the hash. I'm assuming you don't want to add an unnecessary iteration. So it's easier than the above
def my_diff(h2)
dup.delete_if { |k, v| h2[k] == v }
end
This is pretty easy:
a.select {|k, v| b.key?(k) && b[k] != v }
This is O(n), since both key? and Hash#[] are both O(1).

How do I map an array of hashes?

I have an array of hashes:
arr = [ {:a => 1, :b => 2}, {:a => 3, :b => 4} ]
What I want to achieve is:
arr.map{|x| x[:a]}.reduce(:+)
but I think it's a bit ugly, or at least not that elegant as:
arr.map(&:a).reduce(:+)
The later one is wrong because there is no method called a in the hashes.
Are there any better ways to write map{|x| x[:a]}?
You could make actual Objects, possibly with a Struct:
MyClass = Struct.new :a, :b
arr = [MyClass.new(1, 2), MyClass.new(3, 4)]
arr.map(&:a).reduce(:+) #=> 4
Or for more flexibility, an OpenStruct:
require 'ostruct'
arr = [OpenStruct.new(a: 1, b: 2), OpenStruct.new(a: 3, b: 4)]
arr.map(&:a).reduce(:+) #=> 4
Of course either of these can be constructed from existing hashes:
arr = [{ :a => 1, :b => 2 }, { :a => 3, :b => 4 }]
ss = arr.map { |h| h.values_at :a, :b }.map { |attrs| MyClass.new(*attrs) }
ss.map(&:a).reduce(:+) #=> 4
oss = arr.map { |attrs| OpenStruct.new attrs }
oss.map(&:a).reduce(:+) #=> 4
Or, for a more creative, functional approach:
def hash_accessor attr; ->(hash) { hash[attr] }; end
arr = [{ :a => 1, :b => 2 }, { :a => 3, :b => 4 }]
arr.map(&hash_accessor(:a)).reduce(:+) #=> 4
It is unclear what you mean as "better" and why you think the correct version is ugly.
Do you like this "better"?
arr.inject(0) { |sum, h| sum + h[:a] }
There's a way, extending the Symbol.
lib/core_extensions/symbol.rb (credit goes here)
# frozen_string_literal: true
class Symbol
def with(*args, &)
->(caller, *rest) { caller.send(self, *rest, *args, &) }
end
end
Then, given:
arr = [ {:a => 1, :b => 2}, {:a => 3, :b => 4} ]
you can do this:
arr.map(&:[].with(:a)).reduce(:+)
Explanation: to access hash value under any key, you call Hash#[] method. When passed as a :[] (extended) symbol to the Array#map, you can then call .with(*args) on this symbol, effectively passing the parameter (hash key) down to the :[] method. Enjoy.

How to work out frequency of certain key's value in an array of hashes?

I have an array of hashes. Each hash has an uses key. Multiple hashes can share the same uses value.
[{uses => 0},{uses => 1},{uses => 2},{uses => 1},{uses => 0},{uses => 1},{uses => 3}]
How can I generate an array of the most frequent uses values, in a descending order?
[1,0,2,3]
Referencing this discussion of frequency of items in a list, we can easily modify this for your task.
> unsorted = [{:uses=>0}, {:uses=>1}, {:uses=>2}, {:uses=>1}, {:uses=>0}, {:uses=>1}, {:uses=>3}].map{|h| h[:uses]}
> sorted = unsorted.uniq.sort_by{|u| unsorted.grep(u).size}.reverse
=> [1, 0, 2, 3]
hs.inject({}) do |histogram, h|
histogram.merge(h[:uses] => (histogram[h[:uses]] || 0) + 1)
end.sort_by { |k, v| -v }.map { |k, v| k }
# => [1, 0, 2, 3]
I always recommend to use Facets, though:
http://rubyworks.github.com/facets/doc/api/core/Enumerable.html
hs.frequency.sort_by { |k, v| -v }.map { |k, v| k }
# => [1, 0, 2, 3]
Here is a one pass solution:
a = [{:uses => 0},{:uses => 1},{:uses => 2},{:uses => 1},{:uses => 0},
{:uses => 1},{:uses => 3}]
# A hash with the frequency count is formed in one iteration of the array
# followed by the reverse sort and extraction
a.inject(Hash.new(0)) { |h, v| h[v[:uses]] += 1;h}.
sort{|x, y| x <=> y}.map{|kv| kv[0]}

Resources