Array to hash while summing values - ruby

I have a an array as follows:
[[172, 3],
[173, 1],
[174, 2],
[174, 3],
[174, 1]]
That I'd like to convert into an array, but while summing the values for matching keys. So I'd get the following:
{172 => 3, 173 => 1, 174 => 6}
How would I go about doing this?

How would I go about doing this?
Solve one problem at a time.
Given your array:
a = [[172, 3], [173, 1], [174, 2], [174, 3], [174, 1]]
We need an additional hash:
h = {}
Then we have to traverse the pairs in the array:
a.each do |k, v|
if h.key?(k) # If the hash already contains the key
h[k] += v # we add v to the existing value
else # otherwise
h[k] = v # we use v as the initial value
end
end
h #=> {172=>3, 173=>1, 174=>6}
Now let's refactor it. The conditional looks a bit cumbersome, what if we would just add everything?
h = {}
a.each { |k, v| h[k] += v }
#=> NoMethodError: undefined method `+' for nil:NilClass
Bummer, that doesn't work because the hash's values are initially nil. Let's fix that:
h = Hash.new(0) # <- hash with default values of 0
a.each { |k, v| h[k] += v }
h #=> {172=>3, 173=>1, 174=>6}
That looks good. We can even get rid of the temporary variable by using each_with_object:
a.each_with_object(Hash.new(0)) { |(k, v), h| h[k] += v }
#=> {172=>3, 173=>1, 174=>6}

You can try something about:
> array
#=> [[172, 3], [173, 1], [174, 2], [174, 3], [174, 1]]
array.group_by(&:first).map { |k, v| [k, v.map(&:last).inject(:+)] }.to_h
#=> => {172=>3, 173=>1, 174=>6}
Ruby 2.4.0 version:
a.group_by(&:first).transform_values { |e| e.sum(&:last) }
#=> => {172=>3, 173=>1, 174=>6}

For:
array = [[172, 3], [173, 1], [174, 2], [174, 3], [174, 1]]
You could use a hash with a default value 0 like this
hash = Hash.new{|h,k| h[k] = 0 }
Then use it, and sum up values:
array.each { |a, b| hash[a] += b }
#=> {172=>3, 173=>1, 174=>6}

Another possible solution:
arr = [[172, 3], [173, 1], [174, 2], [174, 3], [174, 1]]
hash = arr.each_with_object({}) {|a,h| h[a[0]] = h[a[0]].to_i + a[1]}
p hash
# {172=>3, 173=>1, 174=>6}

Related

Create a hash out of an array where the values are the indices of the elements

I have an array and I want to create a hash whose keys are the elements of the array and whose values are (an array of) the indices of the array. I want to get something like:
array = [1,3,4,5]
... # => {1=>0, 3=>1, 4=>2, 5=>3}
array = [1,3,4,5,6,6,6]
... # => {1=>0, 3=>1, 4=>2, 5=>3, 6=>[4,5,6]}
This code:
hash = Hash.new 0
array.each_with_index do |x, y|
hash[x] = y
end
works fine only if I don't have duplicate elements. When I have duplicate elements, it does not.
Any idea on how I can get something like this?
You can change the logic to special-case the situation when the key already exists, turning it into an array and pushing the new index:
arr = %i{a a b a c}
result = arr.each.with_object({}).with_index do |(elem, memo), idx|
memo[elem] = memo.key?(elem) ? [*memo[elem], idx] : idx
end
puts result
# => {:a=>[0, 1, 3], :b=>2, :c=>4}
It's worth mentioning, though, that whatever you're trying to do here could possibly be accomplished in a different way ... we have no context. In general, it's a good idea to keep key-val data types uniform, e.g. the fact that values here can be numbers or arrays is a bit of a code smell.
Also note that it doesn't make sense to use Hash.new(0) here unless you're intentionally setting a default value (which there's no reason to do). Use {} instead
I'm adding my two cents:
array = [1,3,4,5,6,6,6,8,8,8,9,7,7,7]
hash = {}
array.map.with_index {|val, idx| [val, idx]}.group_by(&:first).map do |k, v|
hash[k] = v[0][1] if v.size == 1
hash[k] = v.map(&:last) if v.size > 1
end
p hash #=> {1=>0, 3=>1, 4=>2, 5=>3, 6=>[4, 5, 6], 8=>[7, 8, 9], 9=>10, 7=>[11, 12, 13]}
It fails with duplicated element not adjacent, of course.
This is the expanded version, step by step, to show how it works.
The basic idea is to build a temporary array with pairs of value and index, then work on it.
array = [1,3,4,5,6,6,6]
tmp_array = []
array.each_with_index do |val, idx|
tmp_array << [val, idx]
end
p tmp_array #=> [[1, 0], [3, 1], [4, 2], [5, 3], [6, 4], [6, 5], [6, 6]]
tmp_hash = tmp_array.group_by { |e| e[0] }
p tmp_hash #=> {1=>[[1, 0]], 3=>[[3, 1]], 4=>[[4, 2]], 5=>[[5, 3]], 6=>[[6, 4], [6, 5], [6, 6]]}
hash = {}
tmp_hash.map do |k, v|
hash[k] = v[0][0] if v.size == 1
hash[k] = v.map {|e| e[1]} if v.size > 1
end
p hash #=> {1=>1, 3=>3, 4=>4, 5=>5, 6=>[4, 5, 6]}
It can be written as one line as:
hash = {}
array.map.with_index.group_by(&:first).map { |k, v| v.size == 1 ? hash[k] = v[0][1] : hash[k] = v.map(&:last) }
p hash
If you are prepared to accept
{ 1=>[0], 3=>[1], 4=>[2], 5=>[3], 6=>[4,5,6] }
as the return value you may write the following.
array.each_with_index.group_by(&:first).transform_values { |v| v.map(&:last) }
#=> {1=>[0], 3=>[1], 4=>[2], 5=>[3], 6=>[4, 5, 6]}
The first step in this calculation is the following.
array.each_with_index.group_by(&:first)
#=> {1=>[[1, 0]], 3=>[[3, 1]], 4=>[[4, 2]], 5=>[[5, 3]], 6=>[[6, 4], [6, 5], [6, 6]]}
This may help readers to follow the subsequent calculations.
I think you will find this return value generally more convenient to use than the one given in the question.
Here are a couple of examples where it's clearly preferable for all values to be arrays. Let:
h_orig = { 1=>0, 3=>1, 4=>2, 5=>3, 6=>[4,5,6] }
h_mod { 1=>[0], 3=>[1], 4=>[2], 5=>[3], 6=>[4,5,6] }
Create a hash h whose keys are unique elements of array and whose values are the numbers of times the key appears in the array
h_mod.transform_values(&:count)
#=> {1=>1, 3=>1, 4=>1, 5=>1, 6=>3}
h_orig.transform_values { |v| v.is_a?(Array) ? v.count : 1 }
Create a hash h whose keys are unique elements of array and whose values equal the index of the first instance of the element in the array.
h_mod.transform_values(&:min)
#=> {1=>0, 3=>1, 4=>2, 5=>3, 6=>4}
h_orig.transform_values { |v| v.is_a?(Array) ? v.min : v }
In these examples, given h_orig, we could alternatively convert values that are indices to arrays containing a single index.
h_orig.transform_values { |v| [*v].count }
h_orig.transform_values { |v| [*v].min }
This is hardly proof that it is generally more convenient for all values to be arrays, but that has been my experience and the experience of many others.

Merging an array of hashes

I have the following array:
x = [ { a: [1,2] }, { a: [3,4] }, { a: [5,6] } ]
and I need to get
{ a: [[1,2], [3,4], [5,6]] }
I have tried to use (among other options) merge:
x.each_with_object({}) do |a, b|
b.merge!(a) {|k, o, n| o.zip(n) }
end
But unfortunately, I get an extra array around the result.
Any suggestions?
THANKS
x.flat_map(&:to_a).group_by(&:first).map{ |k, v| [k, v.map(&:last)] }.to_h
#=> [{:a=>[[1, 2], [3, 4], [5, 6]]}]
It is maybe not the most efficient way to get the expected result but you can do
h = Hash.new([])
x.each { |hash|
hash.each { |key, values|
h[key] = h[key] + [values]
}
}
That way, at the end h is {:a=>[[1, 2], [3, 4], [5, 6]]}
key = x.first.first.first
#=> :a
{ key=>x.map { |h| h[key] } }
#=> {:a=>[[1, 2], [3, 4], [5, 6]]}
Note
a = x.first
#=> {:a=>[1, 2]}
b = a.first
#=> [:a, [1, 2]]
b.first
#=> :a
Another way:
a = x.map { |h| h.merge(h) { |_,v,_| [v] } }
#=> [{:a=>[[1, 2]]}, {:a=>[[3, 4]]}, {:a=>[[5, 6]]}]
a.reduce { |t,h| t.merge(h) { |_,o,n| o+n } }
#=> {:a=>[[1, 2], [3, 4], [5, 6]]}
Both steps use the form of Hash#merge that employs a block to determine the values of keys that are present in both hashes being merged. See the doc for details.
These two steps could be combined into one as follows:
x.reduce { |t,h| t.merge(h.merge(h) { |_,v,_| [v] }) { |_,o,n| o+n } }
#=> {:a=>[1, 2, [3, 4], [5, 6]]}

Result of inject is nil

I'm trying to accumulate some values if they match a condition.
Why is this snippet returning nil, when I would expect it to return 2?
[[1, 2], [2, 3], [3, 8], [4, 2]].inject(0) { |s, e| s + e[1] if e[0] <= 1}
Isn't inject the right method for this?
You should return s;
[[1, 2], [2, 3], [3, 8], [4, 2]].inject(0) { |s, e| s += e[1] if e[0] <= 1; s}
Little cleaner
[[1, 2], [2, 3], [3, 8], [4, 2]].inject(0){|s,(k,v)| s += (k<2 ? v : 0)}
You could do it in multiple steps:
>> a = [[1, 2], [2, 3], [3, 8], [4, 2]]
>> a.select { |e| e.first <= 1 }.inject(0) { |s, e| s += e.last }
=> 2
>> a.select { |e| e.first <= 1 }.map(&:last).inject(0, :+)
=> 2
Doing it all with a single inject should be more efficient but breaking it into pieces might be cleaner and the speed difference won't be noticeable unless you have really large arrays.
If you don't mind emulating a pointer with a Hash or Array, you can do it with each_with_object:
>> a.each_with_object({ :sum => 0 }) { |(k,v), m| m[:sum] += v if k <= 1 }[:sum]
=> 2
>> a.each_with_object([0]) { |(k,v), m| m[0] += v if k <= 1 }.first
=> 2
The result of your inject block is used as the value of s on the next call to your block. On the second iterator, this:
s + e[1] if e[0] <= 1
will have a value of nil because e[0] will be 2. Subsequent iterations also return nil from your block because every e[0] is larger than 1 except the first one. This is why you need to return s from your block. If you had an array like this:
[[1, 2], [2, 3], [3, 8], [4, 2], [1, 11]]
then you wouldn't even get nil out of your inject, you'd just can an exception:
NoMethodError: undefined method `+' for nil:NilClass
when your block tried to add 11 to nil.
Yet another way:
xs.map { |k, v| v if k <= 1 }.compact.inject(0, :+)
Note how Ruby suffers a bit from the lack of list-comprehensions and we have to (somewhat inefficiently) emulate it with map + compact. In a language with LC it'd look better: sum(v for (k, v) in xs if k <= 1).

Ruby: adding second element in array where first element is the same

[[1, 20],[2,30],[1,5],[4,5]]
In ruby how does one go through this array and add the second element if the first is the same with and output such as:
[[1, 25],[2,30],[4,5]]
If order isn't important, inserting the pairs into a hash, adding if the key already exists, and then flattening the hash back into an array is a neat way. In irb:
>> a = [[1, 20],[2,30],[1,5],[4,5]]
=> [[1, 20], [2, 30], [1, 5], [4, 5]]
>> a.inject(Hash.new(0)) { |h, p| h[p[0]] += p[1]; h }.to_a
=> [[1, 25], [2, 30], [4, 5]]
A solution in Ruby:
L = [[1, 20],[2,30],[1,5],[4,5]]
d = {}
L.each{|k,v| d[k] = (d[k]||0) + v}
A solution in Python:
L = [[1, 20],[2,30],[1,5],[4,5]]
d = {}
for k,v in L:
d[k] = d.get(k,0) + v
You can do this (warning, the output is a bit garbled, you may want to convert to array):
myArray = [[1, 20],[2,30],[1,5],[4,5]]
outputHash = {}
myArray.each do |values|
outputHash[values[0]] = outputHash[values[0]].to_i + values[1]
end
puts outputHash

How to count duplicates in Ruby Arrays

How do you count duplicates in a ruby array?
For example, if my array had three a's, how could I count that
Another version of a hash with a key for each element in your array and value for the count of each element
a = [ 1, 2, 3, 3, 4, 3]
h = Hash.new(0)
a.each { | v | h.store(v, h[v]+1) }
# h = { 3=>3, 2=>1, 1=>1, 4=>1 }
Given:
arr = [ 1, 2, 3, 2, 4, 5, 3]
My favourite way of counting elements is:
counts = arr.group_by{|i| i}.map{|k,v| [k, v.count] }
# => [[1, 1], [2, 2], [3, 2], [4, 1], [5, 1]]
If you need a hash instead of an array:
Hash[*counts.flatten]
# => {1=>1, 2=>2, 3=>2, 4=>1, 5=>1}
This will yield the duplicate elements as a hash with the number of occurences for each duplicate item. Let the code speak:
#!/usr/bin/env ruby
class Array
# monkey-patched version
def dup_hash
inject(Hash.new(0)) { |h,e| h[e] += 1; h }.select {
|k,v| v > 1 }.inject({}) { |r, e| r[e.first] = e.last; r }
end
end
# unmonkeey'd
def dup_hash(ary)
ary.inject(Hash.new(0)) { |h,e| h[e] += 1; h }.select {
|_k,v| v > 1 }.inject({}) { |r, e| r[e.first] = e.last; r }
end
p dup_hash([1, 2, "a", "a", 4, "a", 2, 1])
# {"a"=>3, 1=>2, 2=>2}
p [1, 2, "Thanks", "You're welcome", "Thanks",
"You're welcome", "Thanks", "You're welcome"].dup_hash
# {"You're welcome"=>3, "Thanks"=>3}
Simple.
arr = [2,3,4,3,2,67,2]
repeats = arr.length - arr.uniq.length
puts repeats
arr = %w( a b c d c b a )
# => ["a", "b", "c", "d", "c", "b", "a"]
arr.count('a')
# => 2
Another way to count array duplicates is:
arr= [2,2,3,3,2,4,2]
arr.group_by{|x| x}.map{|k,v| [k,v.count] }
result is
[[2, 4], [3, 2], [4, 1]]
requires 1.8.7+ for group_by
ary = %w{a b c d a e f g a h i b}
ary.group_by{|elem| elem}.select{|key,val| val.length > 1}.map{|key,val| key}
# => ["a", "b"]
with 1.9+ this can be slightly simplified because Hash#select will return a hash.
ary.group_by{|elem| elem}.select{|key,val| val.length > 1}.keys
# => ["a", "b"]
To count instances of a single element use inject
array.inject(0){|count,elem| elem == value ? count+1 : count}
arr = [1, 2, "a", "a", 4, "a", 2, 1]
arr.group_by(&:itself).transform_values(&:size)
#=> {1=>2, 2=>2, "a"=>3, 4=>1}
Ruby >= 2.7 solution here:
A new method .tally has been added.
Tallies the collection, i.e., counts the occurrences of each element. Returns a hash with the elements of the collection as keys and the corresponding counts as values.
So now, you will be able to do:
["a", "b", "c", "b"].tally #=> {"a"=>1, "b"=>2, "c"=>1}
What about a grep?
arr = [1, 2, "Thanks", "You're welcome", "Thanks", "You're welcome", "Thanks", "You're welcome"]
arr.grep('Thanks').size # => 3
Its Easy:
words = ["aa","bb","cc","bb","bb","cc"]
One line simple solution is:
words.each_with_object(Hash.new(0)) { |word,counts| counts[word] += 1 }
It works for me.
Thanks!!
I don't think there's a built-in method. If all you need is the total count of duplicates, you could take a.length - a.uniq.length. If you're looking for the count of a single particular element, try
a.select {|e| e == my_element}.length.
Improving #Kim's answer:
arr = [1, 2, "a", "a", 4, "a", 2, 1]
Hash.new(0).tap { |h| arr.each { |v| h[v] += 1 } }
# => {1=>2, 2=>2, "a"=>3, 4=>1}
Ruby code to get the repeated elements in the array:
numbers = [1,2,3,1,2,0,8,9,0,1,2,3]
similar = numbers.each_with_object([]) do |n, dups|
dups << n if seen.include?(n)
seen << n
end
print "similar --> ", similar
Another way to do it is to use each_with_object:
a = [ 1, 2, 3, 3, 4, 3]
hash = a.each_with_object({}) {|v, h|
h[v] ||= 0
h[v] += 1
}
# hash = { 3=>3, 2=>1, 1=>1, 4=>1 }
This way, calling a non-existing key such as hash[5] will return nil instead of 0 with Kim's solution.
I've used reduce/inject for this in the past, like the following
array = [1,5,4,3,1,5,6,8,8,8,9]
array.reduce (Hash.new(0)) {|counts, el| counts[el]+=1; counts}
produces
=> {1=>2, 5=>2, 4=>1, 3=>1, 6=>1, 8=>3, 9=>1}

Resources