Ruby range.reduce with hash accumulator - ruby

I have this method
def heights
(60..68).reduce({}) { |h, i| h.merge!( { %(#{i/12}'#{i%12}") => i } ) }
end
it returns a hash of heights
{
"5'0\"" => 60, "5'1\"" => 61, "5'2\"" => 62,
"5'3\"" => 63, "5'4\"" => 64, "5'5\"" => 65,
"5'6\"" => 66, "5'7\"" => 67, "5'8\"" => 68
}
That's what I want. However, I don't like using the merge! method. I'd much rather use the hash[key] = value syntax for assignment:
def heights
(60..68).reduce({}) { |h, i| h[%(#{i/12}'#{i%12}")] = i }
end
But this code throws errors. I know that with reduce, in your pipes you can name your accumulator and element.
I also understand that
sum = 0
(1..5).each { |i| sum += i }
is equivalent to
(1..5).reduce(0) { |sum, i| sum + i }
So why doesn't this
hash = {}
(1..5).each { |i| hash[i.to_s] = i }
work the same as
(1..5).reduce({}) { |hash, i| hash["#{i}"] = i }

You could use each_with_object instead of reduce:
(60..68).each_with_object({}) { |i, h| h[%(#{i/12}'#{i%12}")] = i }
enumerable.each_with_object(obj) { ... } returns obj so you don't need the artificial-feeling ; h in the block that you'd need with reduce.
Note that the order of the arguments to the block is different than with reduce.

Block in reduce should return new accumulator. In your case
(1..5).reduce({}) { |hash, i| hash["#{i}"] = i }
block returns i, which is an integer, so on the second iteration you will try to call [] on an integer. What you need it this:
(1..5).reduce({}) { |hash, i| hash["#{i}"] = i; hash }

Related

Ruby - sorting hashes into descending order by value

I'm trying to solve the following:
"You are given a dictionary/hash/object containing some languages and your test results in the given languages. Return the list of languages where your test score is at least 60, in descending order of the results.
Examples:
{"Java" => 10, "Ruby" => 80, "Python" => 65} --> ["Ruby", "Python"]
{"Hindi" => 60, "Dutch" => 93, "Greek" => 71} --> ["Dutch", "Greek", "Hindi"]
{"C++" => 50, "ASM" => 10, "Haskell" => 20} --> []
I am having trouble sorting into descending order by results. Here is what I have so far
def my_languages(results)
array = []
results.each { |a,b|
results.values.sort.reverse
if b.to_i >= 60
array << a
end
}
return array
end
Not the most elegant solution but I am a complete Ruby newbie (and Stack Overflow newbie too - sorry!) Any advice would be much appreciated!
You are kinda mixing the sort and filtering out phases. My solution
Filter results with value >= 60
Sort for values (descending, -v)
Extract the first element for every array (the language name)
def my_languages(results)
results.select { |k, v| v >= 60 }.sort_by { |(k,v)| -v }.map(&:first)
end
h = { "Java" => 10, "Ruby" => 80, "Python" => 65 }
h.select { |_,v| v >= 60 }.
keys.
sort_by { |k| -h[k] }
#=> ["Ruby", "Python"]
The steps are as follows.
g = h.select { |_,v| v >= 60 }
#=> {"Ruby"=>80, "Python"=>65}
a = g.keys
#=> ["Ruby", "Python"]
a.sort_by { |k| -h[k] }
#=> ["Ruby", "Python"]
If you don't care for -h[k] two alternative follow.
h.select { |_,v| v >= 60 }.
keys.
sort_by { |k| h[k] }.
reverse
and
a = h.select { |_,v| v >= 60 }.
keys
a.max_by(a.size) { |k| h[k] }
I doubt that one would notice any significant difference in performance among the three.
Enumerable#max_by, min_by, max and min have been permitted to have an optional argument since Ruby v2.2.
To make it faster I would check minimum value when mapping:
hash = {"Hindi" => 60, "Dutch" => 93, "Greek" => 71}
hash.sort.map { |arr| arr[0] if arr[1] >= 60 }.compact
# or imo cleaner
hash.sort.select { |a| a[1] >= 60 }.map(&:first)

about creating hash with certain keys

I am lost. I need to create a hash with keys corresponding to numbers from 1 to 10. The result should be something like this:
my_hash = {1 => "", 2 => "", 3 => ""...}
I have
h = Hash.new
please give me at least one way of doing that, I will need "values" later, for now I just need a hash with keys and I will push values later. Thanks!
h = Hash.new
(1..10).each {|count| h[count] = ''}
What about:
(1..10).map { |x| [x, ''] }.to_h
Or:
{}.tap { |h| 1.step(10) { |i| h[i] = '' } }
Or:
(1..10).zip(Array.new(10) { '' }).to_h
A few more options:
(1..10).each_with_object({}) { |i, h| h[i] = '' }
10.times.each_with_object({}) { |i, h| h[i + 1] = '' }
1.upto(10).each_with_object({}) { |i, h| h[i] = '' }
# Or if you think each_with_object is heresy...
(1..10).inject({}) { |h, i| h.merge!(i => '') } # merge! to avoid wasteful copying
10.times.inject({}) { |h, i| h.merge!(i + 1 => '') }
1.upto(10).inject({}) { |h, i| h.merge!(i => '') }

Any instance of key anywhere in a nested hash

Given a hash like so:
h = {
"actual_amount" => 20,
"otherkey" => "value",
"otherkey2" => [{"actual_amount" => 30, "random_amount" => 45}]
}
where there are any number of layers of nesting, is there a simple way to pluck all the key-value pairs (or just the values) of the keys that are actual_amount?
I've assumed the values of keys are literals or arrays of hashes.
This question clearly calls for a recursive solution.
def amounts(h)
h.each_with_object([]) do |(k,v),a|
case v
when Array
v.each { |g| a.concat amounts(g) }
else
a << v if k == "actual_amount"
end
end
end
Suppose
h = {
"actual_amount"=>20,
1=>2,
2=>[
{ "actual_amount"=>30,
3=>[
{ "actual_amount" => 40 },
{ 4=>5 }
]
},
{ 5=>6 }
]
}
then
amounts(h)
#=> [20, 30, 40]
Using the hash, provided by Cary, as an input:
▶ flatten = ->(inp) do
▷ [*(inp.respond_to?(:map) ? inp.map(&:flatten) : inp)]
▷ end
▶ res = flatten(h).first
▶ res.select.with_index do |_, i|
▷ i > 0 && res[i - 1] == 'actual_amount'
▷ end
#⇒ [20, 30, 40]

Combine hash keys and values if keys are identical except for case

Let's say I have a Ruby Hash where at least two keys are identical, except for their case, for instance:
{ 'Foo' => 1, 'foo' => 2, 'bar' => 3 }
Is there a way I can combine like keys (except for their case) such that the resulting Hash might look like this?
{ 'foo' => 3, 'bar' => 3 }
Thank you!
You can build a new hash:
new_hash = Hash.new(0)
old_hash.each_pair { |k, v| new_hash[k.downcase] += v }
You can use inject to loop all the hash items and build a new hash.
hash = { 'Foo' => 1, 'foo' => 2, 'bar' => 3 }
hash.inject({}) do |result, (key, value)|
key = key.downcase
result[key] = result[key] ? result[key] + value : value
result
end
Here is one more way of doing this
h = { 'Foo' => 1, 'foo' => 2, 'bar' => 3 }
p h.collect{|k, v| {k.downcase => v}}.reduce { |a, v| a.update(v) {|k, o, n| o + n } }
#=> {"foo"=>3, "bar"=>3}
h = { 'Foo' => 1, 'foo' => 2, 'bar' => 3 }
h.each_with_object({}) { |(k,v),g| g[k.downcase] = g[k.downcase].to_i + v }
#=> {"foo"=>3, "bar"=>3}
This makes use of the fact that if g does not have a key e, g[e] on the right side will equal nil and nil.to_i #=> 0. On the other hand, if g has a key e, h[e].to_i will equal h[e].
Another way:
h.each_with_object({}) { |(k,v),g| g.update(k.downcase=>v) { |_,o,v| o+v } }
#=> {"foo"=>3, "bar"=>3}

How to sum values in an array with different hash

I want to sum the total values of the same items in an array.
I have an array as
[{"a"=>1},{"b"=>2},{"c"=>3},{"a"=>2},{"b"=>4}]
I want to get the result as
[{"a"=>3},{"b"=>6},{"c"=>3}]
Which method can do it?
if:
array = [{"a"=>1},{"b"=>2},{"c"=>3},{"a"=>2},{"b"=>4}]
then you can do:
array.inject(Hash.new{|h,k| h[k] = 0})
{ |h, a| k, v = a.flatten; h[k] += v; h }.
map{|arr| Hash[*arr] }
#=> [{"a"=>3}, {"b"=>6}, {"c"=>3}]
or:
array.each_with_object(Hash.new{|h,k| h[k] = 0})
{ |a, h| k, v = a.flatten; h[k] += v }.
map{|arr| Hash[*arr] }
#=> [{"a"=>3}, {"b"=>6}, {"c"=>3}]
It can be done as follows
array.group_by { |h| h.keys.first }.
values.
map {|x| x.reduce({}) { |h1, h2| h1.merge(h2) { |_, o, n| o + n } }
#=> [{"a"=>3}, {"b"=>6}, {"c"=>3}]
Every time you want to transform a collection in not a one-to-one way, it's job for #reduce. For one-to-one transformations we use #map.
array.reduce({}) { |h, acc| acc.merge(h) {|_k, o, n| o+n } }.zip.map(&:to_h)
# => [{"b"=>6}, {"a"=>3}, {"c"=>3}]
Here we use reduce with the initial value {}, which is passed to the block as the acc parameter, and then we use #merge with manual "conflicts resolution". It means that the block is called only when the key we're trying to merge is already present in the method receiver, acc. After that we break the hash into an array of hashes.
There are many ways to do this. It is instructive to see a few, even some that may be unusual and/or not especially efficient.
Here is another way:
arr = [{"a"=>1},{"b"=>2},{"c"=>3},{"a"=>2},{"b"=>4}]
arr.flat_map(&:keys)
.uniq
.map { |k| { k=>arr.reduce(0) { |t,g| t + (g.key?(k) ? g[k] : 0) } } }
#=> [{"a"=>3}, {"b"=>6}, {"c"=>3}]
Since nil.to_i => 0, we could instead write reduce's block as:
{ |t,g| t+g[k].to_i }

Resources