Given a hash that looks like the following:
h = {
"0" => ["1", "true", "21"],
"1" => ["2", "true", "21"],
"2" => ["3", "false", "21"],
"3" => ["4", "true", "22"],
"4" => ["5", "true", "22"],
"5" => ["6", "true", "22"],
"6" => ["7", "false", "21"]
}
I want to find the sum of elements at position 0 across arrays that have the same elements at indices 1 and 2, and return a hash like the following:
{
0 => ["3", "true", "21"],
1 => ["10", "false", "21"],
2 => ["15", "true", "22"]
}
Since there are two arrays with indices 1 and 2 having values "true" and "21", I want to sum the integer values of index 0 for those two arrays, for example.
How can I convert the example hash at the top of this question to the resultant hash below it?
Code
def group_em(h)
h.group_by { |_,v| v.drop(1) }.
transform_values do |a|
a.transpose.
last.
map(&:first).
sum(&:to_i).
to_s
end.
each_with_index.
with_object({}) { |((a,v),i),g| g[i] = [v,*a] }
end
Example
h = {
"0" => ["1", "true", "21"],
"1" => ["2", "true", "21"],
"2" => ["3", "false", "21"],
"3" => ["4", "true", "22"],
"4" => ["5", "true", "22"],
"5" => ["6", "true", "22"],
"6" => ["7", "false", "21"]
}
group_em(h)
#=> {0=>["3", "true", "21"],
# 1=>["10", "false", "21"],
# 2=>["15", "true", "22"]}
Explanation
The major steps
For the hash h above the major steps are as follows.
p = h.group_by { |_,v| v.drop(1) }
#=> {["true", "21"]=>[["0", ["1", "true", "21"]],
# ["1", ["2", "true", "21"]]],
# ["false", "21"]=>[["2", ["3", "false", "21"]],
# ["6", ["7", "false", "21"]]],
# ["true", "22"]=>[["3", ["4", "true", "22"]],
# ["4", ["5", "true", "22"]],
# ["5", ["6", "true", "22"]]]}
q = p.transform_values do |a|
a.transpose.
last.
map(&:first).
sum(&:to_i).
to_s
end
#=> {["true", "21"]=>"3", ["false", "21"]=>"10", ["true", "22"]=>"15"}
enum0 = q.each_with_index
#=> #<Enumerator: {["true", "21"]=>"3", ["false", "21"]=>"10",
# ["true", "22"]=>"15"}:each_with_index>
enum1 = enum0.with_object({})
#=> #<Enumerator: #<Enumerator: {["true", "21"]=>"3", ["false", "21"]=>"10",
# ["true", "22"]=>"15"}:each_with_index>:with_object({})>
enum1.each { |((a,v),i),g| g[i] = [v,*a] }
#=> {0=>["3", "true", "21"],
# 1=>["10", "false", "21"],
# 2=>["15", "true", "22"]}
We can see the values that will be generated and passed to the block by by the enumerator enum1 by converting it to an array:
enum1.to_a
#=> [[[[["true", "21"], "3"], 0], []],
# [[[["false", "21"], "10"], 1], []],
# [[[["true", "22"], "15"], 2], []]]
If you compare the return value for enum0 with that of enum1 you can think of the latter as a compound enumerator, though Ruby does not employ that term.
Details of Hash#transform_values
Now let's look more closely at the calculation of q. The first value of p is passed to the block by Hash#transform_values (which made its debut in MRI 2.4) and becomes the value of the block variable a:
a = p.first.last
#=> [["0", ["1", "true", "21"]], ["1", ["2", "true", "21"]]]
The block calculations are as follows.
b = a.transpose
#=> [["0", "1"], [["1", "true", "21"], ["2", "true", "21"]]]
c = b.last
#=> [["1", "true", "21"], ["2", "true", "21"]]
d = c.map(&:first) # ~same as c.map { |a| a.first }
#=> ["1", "2"]
e = e.sum(&:to_i) # ~same as e.sum { |s| s.to_i }
#=> 3
e.to_s
#=> "3"
We see that the value a has been transformed to "3". The remaining calculations to compute q are similar.
Documentation links
You can find documentation for the methods I've used at the following links for classes Array (drop, transpose, last, first and sum), Integer (to_s), String (to_i) and Enumerator (with_object and next), and the module Enumerable (group_by, map and each_with_index).
Decomposition of nested objects
There is one more tricky-bit I would like to mention. That is the line
enum1.each { |((a,v),i),g| g[i] = [v,*a] }
I've written the block variables in such a way to decompose the values that are generated by the enumerator enum1 and passed to the block. I'm sure that it must look quite imposing for a newbie, but it's not so bad if you take step-by-step, as I will explain.
Firstly, suppose I had a single block variable r (enum1.each { |r|...}). The first value is generated and passed to the block, assigning a value to r:
r = enum1.next
#=> [[[["true", "21"], "3"], 0], []]
We could then execute the following statement in the block to decompose (of disambiguate) r as follows:
((a,v),i),g = r
#=> [[[["true", "21"], "3"], 0], []]
producing the following assignments:
a #=> ["true", "21"]
v #=> "3"
i #=> 0
g #=> []
It is equivalent, and simpler, to replace |r| in the block with |((a,v),i),g|.
If you study the locations of the brackets in the nested array produced by enum1.next you will see how I determined where I needed parentheses when writing the block variables. This decomposition of nested arrays and other objects is a very convenient and powerful feature or Ruby, one that is much underused.
I am not a ruby developer so I can't suggest any best practices but simple algorithm that comes in my mind after reading this, is to create a new hash and check if array values are in it or not, if not then append new value like this.
h = {
"0" => ["1", "true", "21"],
"1" => ["2", "true", "21"],
"2" => ["3", "false", "21"],
"3" => ["4", "true", "22"],
"4" => ["5", "true", "22"],
"5" => ["6", "true", "22"],
"6" => ["7", "false", "21"]
}
new_h = {}
h.each do |key, val|
x1 = val.at(1)
x2 = val.at(2)
found = false
new_h.each do |key1, val2|
y1 = val2.at(1)
y2 = val2.at(2)
if x1 === y1 && x2 === y2
found = true
arr = [val2.at(0).to_i + val.at(0).to_i, x1, x2]
new_h[key1] = arr
end
end
if !found
new_h[new_h.length] = val
end
if new_h.empty?
new_h[key] = val
end
end
puts "#{new_h}"
Just out of curiosity.
input.
values.
map { |i, *rest| [rest, i.to_i] }.
group_by(&:shift).
map do |*key, values|
[values.flatten.sum.to_s, *key.flatten]
end
references : Enumerable#group_by, Enumerator#with_index, Array#to_h
key_sum = ->(group) { group.sum { |key, _| key.to_i }.to_s }
given_hash.values.group_by { |_, *rest| rest }.
map.with_index { |(key, group), idx| [idx, [key_sum.call(group), *key]] }.to_h
#=> {0=>["3", "true", "21"], 1=>["10", "false", "21"], 2=>["15", "true", "22"]}
group by
given_hash.values.group_by { |_, *rest| rest }
#=> { ["true", "21"] => [["1", "true", "21"], ["2", "true", "21"]]...
key_sum function
key_sum = ->(group) { group.sum { |key, _| key.to_i }.to_s }
key_sum.call([["1", "true", "21"], ["2", "true", "21"]]) #=> '3'
to_h
[[0, ["3", "true", "21"]], [1, ["10", "false", "21"]], [2, ["15", "true", "22"]]].to_h
#=> {0=>["3", "true", "21"], 1=>["10", "false", "21"], 2=>["15", "true", "22"]}
This should answer your question, despite being a very long way of solving it. I'm sure there are shortcuts to solve it more easily but you requested a clear explanation of what's happening and I hope that this guides you through how to solve it.
# Start with a hash
hash = {
"0" => ["1", "true", "21"],
"1" => ["2", "true", "21"],
"2" => ["3", "false", "21"],
"3" => ["4", "true", "22"],
"4" => ["5", "true", "22"],
"5" => ["6", "true", "22"],
"6" => ["7", "false", "21"]
}
# Extract just the values from the hash into an array
values = hash.values
added_values = values.map do |array|
# Find all arrays that match this array's values at indices [1] and [2]
matching = values.select { |a| a[1] == array[1] && a[2] == array[2] }
sum = 0
# Add the values at index 0 from each matching array
matching.each { |match| sum += match[0].to_i }
# Return a new array with these values
[sum.to_s, array[1], array[2]]
end
# Reject any duplicative arrays
added_values.uniq!
# Convert the array back to a hash
added_values.each_with_index.each_with_object({}) { |(array, index), hash| hash[index] = array }
Hash and Array have most powerful in-built functions in ruby.
z = h.group_by { |k,v| v[1..2] }.keep_if { |k,v| v.length > 1 }
val = z.map { |k,v| [v.map { |x| x[1] }.map(&:first).map(&:to_i).inject(:+).to_s, k[0], k[1]] }
val.each_with_index.inject({}) { |m,(x,i)| m[i] = x; m }
=> {0 =>["3", "true", "21"], 1 =>["10", "false", "21"], 2 =>["15", "true", "22"]}
If you know these functions then you do not need complex implementation ever. Happy learning :)
h
.values
.group_by{|_, *a| a}
.map
.with_index{|(k, a), i| [i, [a.inject(0){|acc, (n, *)| acc + n.to_i}.to_s, *k]]}
.to_h
# => {0=>["3", "true", "21"], 1=>["10", "false", "21"], 2=>["15", "true", "22"]}
Suppose I have a hash
#attribute_type = {
typeA: ['a', 'b', 'c'],
typeB: ['1', '2', '3'],
typeC: ['9', '8', '7']
}
I want to iterate over the values so I can create an array having all distinct possible combinations of the three arrays, for example:
['a', '1', '9'], ['a', '1', '8'], ['a', '1', '7'], ['a', '2', '9'], ...
Is this possible?
h = { :typeA=>['a','b','c'], :typeB=>['1','2','3'], :typeC=>['9','8','7'] }
first, *rest = h.values
#=> [["a", "b", "c"], ["1", "2", "3"], ["9", "8", "7"]]
first.product(*rest)
#=> [["a", "1", "9"], ["a", "1", "8"], ["a", "1", "7"],
# ["a", "2", "9"], ["a", "2", "8"], ["a", "2", "7"],
# ["a", "3", "9"], ["a", "3", "8"], ["a", "3", "7"],
# ["b", "1", "9"], ["b", "1", "8"], ["b", "1", "7"],
# ["b", "2", "9"], ["b", "2", "8"], ["b", "2", "7"],
# ["b", "3", "9"], ["b", "3", "8"], ["b", "3", "7"],
# ["c", "1", "9"], ["c", "1", "8"], ["c", "1", "7"],
# ["c", "2", "9"], ["c", "2", "8"], ["c", "2", "7"],
# ["c", "3", "9"], ["c", "3", "8"], ["c", "3", "7"]]
See Array#product.
My 2 cents.
Pretty the same as Cary Swoveland, but just one line:
h.values.first.product(*h.values.drop(1))
I'm trying to make a loop that when the random number matches the same index of the input the value is changed and stays that way for each following loop.
input = gets.chomp
tries_left = 12
while(tries_left > 0)
tries_left -= 1
computer = 4.times.map do rand(0..6) end.join
if computer[0] == input[0]
computer[0] = input[0]
end
end
in the code above after the first loop the value stored to input[0] resets.
computer = 4.times.map do rand(0..6) end.join
input = gets.chomp
tries_left = 12
while(tries_left > 0)
tries_left -= 1
if computer[0] == input[0]
computer[0] = input[0]
end
if I take computer out of the loop like this, it will have the same random number generated each time. Once again I need it to generate new numbers each time besides what was already a match.
If you make the computer an array of strings, you can freeze it to prevent further modifications to it, and then replace the contents in computer when it doesn't match the index:
input = gets.chomp
tries_left = 12
computer = Array.new(4) { '' }
# setting the srand to 1234, the next 48 calls to 'rand(0..6)' will always
# result in the following sequence:
# 3, 6, 5, 4, 4, 0, 1, 1, 1, 2, 6, 3, 6, 4, 4, 2, 6, 2, 0, 0, 4, 5, 0, 1,
# 6, 6, 2, 0, 3, 4, 5, 2, 6, 2, 3, 3, 0, 1, 3, 0, 3, 2, 3, 4, 1, 3, 3, 3
# this is useful for testing things are working correctly,
# but take it out for 'live' code
srand 1234
while tries_left > 0
# no need to keep iterating if we've generated all the correct values
if computer.all?(&:frozen?)
puts "won #{computer.inspect} in #{12 - tries_left} tries"
break
end
tries_left -= 1
computer.each.with_index do |random, index|
# generate a new random number here unless they guessed correctly previously
random.replace(rand(0..6).to_s) unless random.frozen?
# if they've guessed the new random number, mark the string so they we
# don't update it
random.freeze if random == input[index]
end
puts "#{computer.inspect} has #{computer.count(&:frozen?)} correct numbers"
end
and then when you run the script:
$ echo 3654 | ruby example.rb
# ["3", "6", "5", "4"] has 4 correct numbers
# won ["3", "6", "5", "4"] in 1 tries
$ echo 3644 | ruby example.rb
# ["3", "6", "5", "4"] has 3 correct numbers
# ["3", "6", "4", "4"] has 4 correct numbers
# won ["3", "6", "4", "4"] in 2 tries
$ echo 3555 | ruby example.rb
# ["3", "6", "5", "4"] has 2 correct numbers
# ["3", "4", "5", "0"] has 2 correct numbers
# ["3", "1", "5", "1"] has 2 correct numbers
# ["3", "1", "5", "2"] has 2 correct numbers
# ["3", "6", "5", "3"] has 2 correct numbers
# ["3", "6", "5", "4"] has 2 correct numbers
# ["3", "4", "5", "2"] has 2 correct numbers
# ["3", "6", "5", "2"] has 2 correct numbers
# ["3", "0", "5", "0"] has 2 correct numbers
# ["3", "4", "5", "5"] has 3 correct numbers
# ["3", "0", "5", "5"] has 3 correct numbers
# ["3", "1", "5", "5"] has 3 correct numbers
It’s not quite clear what you are trying to accomplish, but this:
if computer[0] == input[0]
computer[0] = input[0]
end
is obviously a noop. Nothing gets updated, since the computer[0], whatever it is, is being set to the same value it was. I believe you wanted to somehow use an index in the array:
4.times.map do |index|
value = rand(0..6)
# somehow check the similarity, e.g.:
if input[index] == value
# do something
end
end
I beg your pardon for the very vague answer, but it is really hard to understand the goal you are trying to achieve.