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"]}
I would like to split a string into array groups of three as shown in desired output. Using Array#each_slice like this 1_223_213_213.to_s.split('').each_slice(3){|arr| p arr }
Current output: Desired output
# ["1", "2", "2"] # ["0", "0", "1"]
# ["3", "2", "1"] # ["2", "2", "3"]
# ["3", "2", "1"] # ["2", "1", "3"]
# ["3"] # ["2", "1", "3"]
Must work with numbers from (0..trillion). I posted my solution as an answer below. Hoping you all can give me some suggestion(s) to optimize or alternative implements?
Try left-padding with zeros until the string length is an even multiple of your "slice" target:
def slice_with_padding(s, n=3, &block)
s = "0#{s}" while s.to_s.size % n != 0
s.to_s.chars.each_slice(n, &block)
end
slice_with_padding(1_223_213_213) { |x| puts x.inspect }
# ["0", "0", "1"]
# ["2", "2", "3"]
# ["2", "1", "3"]
# ["2", "1", "3"]
slice_with_padding(12_345, 4) { |x| puts x.inspect }
# ["0", "0", "0", "1"]
# ["2", "3", "4", "5"]
You might find this a little more pleasing to your eye:
def slice_by_3(n)
n = n.to_s
l = n.length
[*n.rjust(l % 3 == 0 ? l : l + 3 - l % 3, '0').chars.each_slice(3)]
end
slice_by_3 2_123_456_544_545_355
=> [["0", "0", "2"],
["1", "2", "3"],
["4", "5", "6"],
["5", "4", "4"],
["5", "4", "5"],
["3", "5", "5"]]
Alternatively, if you want a more general solution:
def slice_by_n(num, n=3)
num = num.to_s
l = num.length
[*num.rjust(l % n == 0 ? l : l + n - l % n, '0').chars.each_slice(n)]
end
Here is a possible solution for the problem:
def slice_by_3 number
initial_number = number.to_s.split('').size
number = "00#{number}" if initial_number == 1
modulus = number.to_s.split(/.{3}/).size
padleft = '0' * ( (modulus*3) % number.to_s.split('').size )
("#{padleft}#{number}").split('').each_slice(3){|arr| p arr }
end
slice_by_3 2_123_456_544_545_355
# ["0", "0", "2"]
# ["1", "2", "3"]
# ["4", "5", "6"]
# ["5", "4", "4"]
# ["5", "4", "5"]
# ["3", "5", "5"]
Just seems somewhat complex and I want to believe there is a better way. I appreciate your feedback.
def slice_by_3 number
"000#{number}".split('').reverse
.each_slice(3).to_a[0..-2].reverse
.each { |arr| p arr.reverse }
end
slice_by_3 13_456_544_545_355
# ["0", "1", "3"]
# ["4", "5", "6"]
# ["5", "4", "4"]
# ["5", "4", "5"]
# ["3", "5", "5"]
This code reverses the whole array after adding 3 zeroes to the number start. each_slice(3) then slices to the proper groups (although reversed) plus one which consists of either ["0","0","0"], ["0","0"] or ["0"] depending on the original length of the number.
[0..-2] cuts the last group of zeroes. Then the groups are reversed back, and each group is printed (reversed back).
Here are a couple methods
n = 1_223_213_213.to_s
n.rjust(n.size + n.size % 3,"0").chars.each_slice(3).to_a
OR
n.rjust(15,"0").chars.each_slice(3).drop_while{|a| a.join == "000"}
15 is because you stated the max was a trillion obviously this number means very little as it rejects all results that contain all zeros so any number greater than 15 that is divisible by 3 will work for your example
Another way:
def split_nbr(n)
str = n.to_s
len = str.size
str.rjust(len + (3-len%3)%3, '0').scan(/.../).map(&:chars)
end
split_nbr( 1_223_213_213)
#=> [["0", "0", "1"], ["2", "2", "3"], ["2", "1", "3"], ["2", "1", "3"]]
split_nbr( 11_223_213_213)
#=> [["0", "1", "1"], ["2", "2", "3"], ["2", "1", "3"], ["2", "1", "3"]]
split_nbr(111_223_213_213)
#=> [["1", "1", "1"], ["2", "2", "3"], ["2", "1", "3"], ["2", "1", "3"]]